diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 0f759fd93862..ab03432a93ee 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -234,6 +234,7 @@ fn sample_thread_start_response( model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + project_trust_established: false, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnRequest, diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index ac50d6ebc7e2..4be0d002c7c4 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -309,6 +309,7 @@ fn sample_thread_start_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + project_trust_established: false, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnRequest, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 1102135a7184..a03100ccc609 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -19657,6 +19657,11 @@ "modelProvider": { "type": "string" }, + "projectTrustEstablished": { + "default": false, + "description": "Whether this request changed the active project from having no explicit trust level to trusted.", + "type": "boolean" + }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 29dfe78034f9..2c5bf28e8dfc 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -17436,6 +17436,11 @@ "modelProvider": { "type": "string" }, + "projectTrustEstablished": { + "default": false, + "description": "Whether this request changed the active project from having no explicit trust level to trusted.", + "type": "boolean" + }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 3ef1577038df..e31c9eb4100f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2436,6 +2436,11 @@ "modelProvider": { "type": "string" }, + "projectTrustEstablished": { + "default": false, + "description": "Whether this request changed the active project from having no explicit trust level to trusted.", + "type": "boolean" + }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 992ab5dba7d0..e25bca4d9db4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -10,6 +10,10 @@ import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** + * Whether this request changed the active project from having no explicit + * trust level to trusted. + */ +projectTrustEstablished: boolean, /** * Environment-native paths to instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e693e8d4514a..0319fc4b4f03 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2608,6 +2608,7 @@ mod tests { model_provider: "openai".to_string(), service_tier: None, cwd, + project_trust_established: false, runtime_workspace_roots: Vec::new(), instruction_sources: vec![ codex_utils_path_uri::LegacyAppPathString::from_abs_path(&absolute_path( @@ -2661,6 +2662,7 @@ mod tests { "modelProvider": "openai", "serviceTier": null, "cwd": absolute_path_string("tmp"), + "projectTrustEstablished": false, "runtimeWorkspaceRoots": [], "instructionSources": [absolute_path_string("tmp/AGENTS.md")], "approvalPolicy": "on-request", diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 367a4a64c8f2..1c5ed687d3d3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -173,6 +173,10 @@ pub struct ThreadStartResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Whether this request changed the active project from having no explicit + /// trust level to trusted. + #[serde(default)] + pub project_trust_established: bool, /// Thread-scoped runtime workspace roots used to materialize /// `:workspace_roots`. #[experimental("thread/start.runtimeWorkspaceRoots")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 6ee6baf2dc6c..fdbeccb0bb7d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -138,6 +138,7 @@ Example with notification opt-out: ## API Overview - `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client. + The response field `projectTrustEstablished` is `true` only when this request changes the project from unspecified trust to trusted; repeated starts, explicitly untrusted projects, and starts without trust-granting permissions return `false`. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` is rejected. If `lastTurnId` is null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. - `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior. diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index af8525873f52..ed9a9766c05c 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1079,6 +1079,7 @@ impl ThreadRequestProcessor { .load_with_overrides(config_overrides.clone(), typesafe_overrides.clone()) .await .map_err(|err| config_load_error(&err))?; + let mut project_trust_established = false; // The user may have requested WorkspaceWrite or DangerFullAccess via // the command line, though in the process of deriving the Config, it @@ -1096,6 +1097,7 @@ impl ThreadRequestProcessor { && config.active_project.trust_level.is_none() && (requested_permissions_trust_project || effective_permissions_trust_project) { + project_trust_established = true; let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd) .await .unwrap_or_else(|| config.cwd.clone()); @@ -1303,6 +1305,7 @@ impl ThreadRequestProcessor { model_provider: config_snapshot.model_provider_id, service_tier: config_snapshot.service_tier, cwd, + project_trust_established, runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: config_snapshot.approval_policy.into(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index f96653b02ab3..aa5896e198b9 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -1286,11 +1286,16 @@ model_reasoning_effort = "high" ..Default::default() }) .await?; - timeout( + let first_response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(first_request)), ) .await??; + let ThreadStartResponse { + project_trust_established, + .. + } = to_response::(first_response)?; + assert!(project_trust_established); let second_request = mcp .send_thread_start_request(ThreadStartParams { @@ -1305,11 +1310,13 @@ model_reasoning_effort = "high" .await??; let ThreadStartResponse { approval_policy, + project_trust_established, reasoning_effort, .. } = to_response::(second_response)?; assert_eq!(approval_policy, AskForApproval::OnRequest); + assert!(!project_trust_established); assert_eq!(reasoning_effort, Some(ReasoningEffort::High)); let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?; diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 97ded50525f8..4c5b25d77e4d 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -782,6 +782,7 @@ fn sample_thread_start_response() -> ThreadStartResponse { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + project_trust_established: false, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: codex_app_server_protocol::AskForApproval::OnRequest,