Skip to content

Commit 10b6650

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 72b8aa9 + 88165e1 commit 10b6650

250 files changed

Lines changed: 13143 additions & 2593 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ In the codex-rs folder where the rust code lives:
2323
- When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable.
2424
- Prefer private modules and explicitly exported public crate API.
2525
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
26+
- When working with MCP tool calls, prefer using `codex-rs/codex-mcp/src/mcp_connection_manager.rs` to handle mutation of tools and tool calls. Aim to minimize the footprint of changes and leverage existing abstractions rather than plumbing code through multiple levels of function calls.
2627
- If you change Rust dependencies (`Cargo.toml` or `Cargo.lock`), run `just bazel-lock-update` from the
2728
repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
2829
- After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught

codex-rs/Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
members = [
3+
"account",
34
"analytics",
45
"backend-client",
56
"ansi-escape",
@@ -104,6 +105,7 @@ license = "Apache-2.0"
104105
# Internal
105106
app_test_support = { path = "app-server/tests/common" }
106107
codex-analytics = { path = "analytics" }
108+
codex-account = { path = "account" }
107109
codex-ansi-escape = { path = "ansi-escape" }
108110
codex-api = { path = "codex-api" }
109111
codex-app-server = { path = "app-server" }

codex-rs/account/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
load("//:defs.bzl", "codex_rust_crate")
2+
3+
codex_rust_crate(
4+
name = "account",
5+
crate_name = "codex_account",
6+
)

codex-rs/account/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "codex-account"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
publish = false
7+
8+
[lib]
9+
name = "codex_account"
10+
path = "src/lib.rs"
11+
12+
[lints]
13+
workspace = true
14+
15+
[dependencies]
16+
anyhow = { workspace = true }
17+
codex-backend-client = { workspace = true }
18+
codex-login = { workspace = true }
19+
thiserror = { workspace = true }

codex-rs/account/src/lib.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use codex_backend_client::Client as BackendClient;
2+
use codex_backend_client::RequestError;
3+
use codex_backend_client::WorkspaceRole as BackendWorkspaceRole;
4+
use codex_login::AuthManager;
5+
use codex_login::CodexAuth;
6+
use thiserror::Error;
7+
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub enum WorkspaceRole {
10+
AccountOwner,
11+
AccountAdmin,
12+
StandardUser,
13+
}
14+
15+
impl WorkspaceRole {
16+
fn is_workspace_owner(self) -> bool {
17+
matches!(self, Self::AccountOwner | Self::AccountAdmin)
18+
}
19+
}
20+
21+
impl From<BackendWorkspaceRole> for WorkspaceRole {
22+
fn from(value: BackendWorkspaceRole) -> Self {
23+
match value {
24+
BackendWorkspaceRole::AccountOwner => Self::AccountOwner,
25+
BackendWorkspaceRole::AccountAdmin => Self::AccountAdmin,
26+
BackendWorkspaceRole::StandardUser => Self::StandardUser,
27+
}
28+
}
29+
}
30+
31+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32+
pub struct WorkspaceOwnership {
33+
pub workspace_role: Option<WorkspaceRole>,
34+
pub is_workspace_owner: Option<bool>,
35+
}
36+
37+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38+
pub enum AddCreditsNudgeEmailStatus {
39+
Sent,
40+
CooldownActive,
41+
}
42+
43+
#[derive(Debug, Error)]
44+
pub enum SendAddCreditsNudgeEmailError {
45+
#[error("codex account authentication required to notify workspace owner")]
46+
AuthRequired,
47+
48+
#[error("chatgpt authentication required to notify workspace owner")]
49+
ChatGptAuthRequired,
50+
51+
#[error("failed to construct backend client: {0}")]
52+
CreateClient(#[from] anyhow::Error),
53+
54+
#[error("failed to notify workspace owner: {0}")]
55+
Request(#[from] RequestError),
56+
}
57+
58+
pub async fn send_add_credits_nudge_email(
59+
chatgpt_base_url: impl Into<String>,
60+
auth_manager: &AuthManager,
61+
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
62+
let auth = auth_manager
63+
.auth()
64+
.await
65+
.ok_or(SendAddCreditsNudgeEmailError::AuthRequired)?;
66+
send_add_credits_nudge_email_for_auth(chatgpt_base_url, &auth).await
67+
}
68+
69+
pub async fn send_add_credits_nudge_email_for_auth(
70+
chatgpt_base_url: impl Into<String>,
71+
auth: &CodexAuth,
72+
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
73+
if !auth.is_chatgpt_auth() {
74+
return Err(SendAddCreditsNudgeEmailError::ChatGptAuthRequired);
75+
}
76+
77+
let client = BackendClient::from_auth(chatgpt_base_url, auth)?;
78+
match client.send_add_credits_nudge_email().await {
79+
Ok(()) => Ok(AddCreditsNudgeEmailStatus::Sent),
80+
Err(err) if err.status().is_some_and(|status| status.as_u16() == 429) => {
81+
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
82+
}
83+
Err(err) => Err(err.into()),
84+
}
85+
}
86+
87+
pub async fn resolve_workspace_role_and_owner_for_auth(
88+
chatgpt_base_url: &str,
89+
auth: Option<&CodexAuth>,
90+
) -> WorkspaceOwnership {
91+
let token_is_workspace_owner = auth.and_then(CodexAuth::is_workspace_owner);
92+
let Some(auth) = auth else {
93+
return WorkspaceOwnership::default();
94+
};
95+
96+
let workspace_role = fetch_current_workspace_role_for_auth(chatgpt_base_url, auth).await;
97+
let is_workspace_owner = workspace_role
98+
.map(WorkspaceRole::is_workspace_owner)
99+
.or(token_is_workspace_owner);
100+
WorkspaceOwnership {
101+
workspace_role,
102+
is_workspace_owner,
103+
}
104+
}
105+
106+
async fn fetch_current_workspace_role_for_auth(
107+
chatgpt_base_url: &str,
108+
auth: &CodexAuth,
109+
) -> Option<WorkspaceRole> {
110+
if !auth.is_chatgpt_auth() {
111+
return None;
112+
}
113+
114+
let client = BackendClient::from_auth(chatgpt_base_url.to_string(), auth).ok()?;
115+
client
116+
.get_current_workspace_role()
117+
.await
118+
.ok()
119+
.flatten()
120+
.map(WorkspaceRole::from)
121+
}

codex-rs/analytics/src/analytics_client_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ fn subagent_thread_started_review_serializes_expected_shape() {
454454
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
455455
SubAgentThreadStartedInput {
456456
thread_id: "thread-review".to_string(),
457+
parent_thread_id: None,
457458
product_client_id: "codex-tui".to_string(),
458459
client_name: "codex-tui".to_string(),
459460
client_version: "1.0.0".to_string(),
@@ -496,6 +497,7 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
496497
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
497498
SubAgentThreadStartedInput {
498499
thread_id: "thread-spawn".to_string(),
500+
parent_thread_id: None,
499501
product_client_id: "codex-tui".to_string(),
500502
client_name: "codex-tui".to_string(),
501503
client_version: "1.0.0".to_string(),
@@ -526,6 +528,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
526528
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
527529
SubAgentThreadStartedInput {
528530
thread_id: "thread-memory".to_string(),
531+
parent_thread_id: None,
529532
product_client_id: "codex-tui".to_string(),
530533
client_name: "codex-tui".to_string(),
531534
client_version: "1.0.0".to_string(),
@@ -550,6 +553,7 @@ fn subagent_thread_started_other_serializes_expected_shape() {
550553
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
551554
SubAgentThreadStartedInput {
552555
thread_id: "thread-guardian".to_string(),
556+
parent_thread_id: None,
553557
product_client_id: "codex-tui".to_string(),
554558
client_name: "codex-tui".to_string(),
555559
client_version: "1.0.0".to_string(),
@@ -562,6 +566,31 @@ fn subagent_thread_started_other_serializes_expected_shape() {
562566

563567
let payload = serde_json::to_value(&event).expect("serialize other subagent event");
564568
assert_eq!(payload["event_params"]["subagent_source"], "guardian");
569+
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
570+
}
571+
572+
#[test]
573+
fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
574+
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
575+
SubAgentThreadStartedInput {
576+
thread_id: "thread-guardian".to_string(),
577+
parent_thread_id: Some("parent-thread-guardian".to_string()),
578+
product_client_id: "codex-tui".to_string(),
579+
client_name: "codex-tui".to_string(),
580+
client_version: "1.0.0".to_string(),
581+
model: "gpt-5".to_string(),
582+
ephemeral: false,
583+
subagent_source: SubAgentSource::Other("guardian".to_string()),
584+
created_at: 126,
585+
},
586+
));
587+
588+
let payload = serde_json::to_value(&event).expect("serialize guardian subagent event");
589+
assert_eq!(payload["event_params"]["subagent_source"], "guardian");
590+
assert_eq!(
591+
payload["event_params"]["parent_thread_id"],
592+
"parent-thread-guardian"
593+
);
565594
}
566595

567596
#[tokio::test]
@@ -574,6 +603,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
574603
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
575604
SubAgentThreadStartedInput {
576605
thread_id: "thread-review".to_string(),
606+
parent_thread_id: None,
577607
product_client_id: "codex-tui".to_string(),
578608
client_name: "codex-tui".to_string(),
579609
client_version: "1.0.0".to_string(),

codex-rs/analytics/src/events.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ pub(crate) fn subagent_thread_started_event_request(
249249
thread_source: Some("subagent"),
250250
initialization_mode: ThreadInitializationMode::New,
251251
subagent_source: Some(subagent_source_name(&input.subagent_source)),
252-
parent_thread_id: subagent_parent_thread_id(&input.subagent_source),
252+
parent_thread_id: input
253+
.parent_thread_id
254+
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
253255
created_at: input.created_at,
254256
};
255257
ThreadInitializedEvent {

codex-rs/analytics/src/facts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub struct AppInvocation {
5454
#[derive(Clone)]
5555
pub struct SubAgentThreadStartedInput {
5656
pub thread_id: String,
57+
pub parent_thread_id: Option<String>,
5758
pub product_client_id: String,
5859
pub client_name: String,
5960
pub client_version: String,

codex-rs/app-server-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ codex-core = { workspace = true }
1919
codex-exec-server = { workspace = true }
2020
codex-feedback = { workspace = true }
2121
codex-protocol = { workspace = true }
22+
codex-utils-rustls-provider = { workspace = true }
2223
futures = { workspace = true }
2324
serde = { workspace = true }
2425
serde_json = { workspace = true }

0 commit comments

Comments
 (0)