Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7bd45ba
exec-server: preserve empty workspace roots
pakrym-oai Jul 9, 2026
5092b60
core: forward workspace roots to remote commands
pakrym-oai Jul 9, 2026
8c24dcb
core: allow time for remote workspace root test
pakrym-oai Jul 9, 2026
ec7065b
core: restore workspace root test timeout
pakrym-oai Jul 9, 2026
2c98e1f
core: avoid waiting twice for workspace test turn
pakrym-oai Jul 9, 2026
0e31de2
core: run workspace root tests locally and remotely
pakrym-oai Jul 9, 2026
aa74579
core: expose local sandbox helper to workspace tests
pakrym-oai Jul 9, 2026
9f75bf7
core: improve workspace root test diagnostics
pakrym-oai Jul 9, 2026
500e176
core: retain workspace command diagnostics
pakrym-oai Jul 9, 2026
27f21a7
core: rely on sandbox helper visibility
pakrym-oai Jul 9, 2026
dd51887
core: test workspace roots with write permissions
pakrym-oai Jul 10, 2026
014742d
core: move workspace roots onto environments
pakrym-oai Jul 8, 2026
b008d04
core: require roots when constructing turn environments
pakrym-oai Jul 9, 2026
e1cb296
core: expose workspace roots to tool sandboxing
pakrym-oai Jul 9, 2026
2bb2cd2
core: require tool runtimes to provide workspace roots
pakrym-oai Jul 9, 2026
6e417ed
app-server: keep workspace roots environment-local
pakrym-oai Jul 9, 2026
7e95377
core: keep session permission profiles symbolic
pakrym-oai Jul 9, 2026
884c235
app-server: preserve legacy workspace roots coverage
pakrym-oai Jul 9, 2026
8a2621d
app-server: keep permission profile roots symbolic
pakrym-oai Jul 9, 2026
fc49471
app-server: preserve environment root compatibility
pakrym-oai Jul 9, 2026
872e688
app-server: preserve legacy cwd root updates
pakrym-oai Jul 10, 2026
4bc7a10
app-server: simplify legacy workspace root updates
pakrym-oai Jul 10, 2026
2020f57
app-server: trim environment root plumbing
pakrym-oai Jul 10, 2026
8fbc671
test: assert selected environment roots directly
pakrym-oai Jul 10, 2026
31fbc7a
core: render workspace roots for foreign environments
pakrym-oai Jul 10, 2026
a6c4d49
Revert "core: render workspace roots for foreign environments"
pakrym-oai Jul 10, 2026
c69a8a8
test: drop legacy workspace root assertion
pakrym-oai Jul 10, 2026
6507c02
core: preserve effective session permissions event
pakrym-oai Jul 10, 2026
c5b8161
app-server: preserve local environment cwd fallback
pakrym-oai Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions codex-rs/app-server-protocol/src/protocol/v2/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4217,13 +4217,15 @@ fn turn_start_params_round_trip_environments() {
let raw_cwd = r"C:\workspace";
let cwd: LegacyAppPathString =
serde_json::from_value(json!(raw_cwd)).expect("API path should deserialize");
let workspace_root = cwd.clone();
let params: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread_123",
"input": [],
"environments": [
{
"environmentId": "local",
"cwd": cwd
"cwd": cwd,
"runtimeWorkspaceRoots": [workspace_root]
}
],
}))
Expand All @@ -4234,6 +4236,7 @@ fn turn_start_params_round_trip_environments() {
Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: cwd.clone(),
runtime_workspace_roots: Some(vec![workspace_root.clone()]),
}])
);
assert_eq!(
Expand All @@ -4247,7 +4250,8 @@ fn turn_start_params_round_trip_environments() {
Some(&json!([
{
"environmentId": "local",
"cwd": cwd
"cwd": cwd,
"runtimeWorkspaceRoots": [workspace_root]
}
]))
);
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub enum TurnStatus {
pub struct TurnEnvironmentParams {
pub environment_id: String,
pub cwd: LegacyAppPathString,
/// Environment-native runtime workspace roots. Omitted defaults to `cwd`.
#[ts(optional = nullable)]
pub runtime_workspace_roots: Option<Vec<LegacyAppPathString>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,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.
- `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` supplies the runtime workspace roots used when app-server creates default environment selections; 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` and optional environment-native `runtimeWorkspaceRoots`. Explicit environments ignore the top-level roots; omitted per-environment roots default to that environment's `cwd`, while an empty list explicitly selects no roots. 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.
- `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.
Expand Down Expand Up @@ -168,7 +168,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
- `thread/rollback` — deprecated and will be removed soon. Drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` supplies the default roots for newly resolved environment selections. Explicit `environments[].runtimeWorkspaceRoots` override that fallback with environment-native absolute paths. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior.
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,28 @@ fn resolve_turn_environment_selections(
environment.cwd
))
})?;
let workspace_roots = environment
.runtime_workspace_roots
.map(|roots| {
let mut resolved_roots = Vec::new();
for root in roots {
Comment on lines +583 to +587

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Bound environment workspace roots before accepting them

When a v2 client supplies environments, each runtimeWorkspaceRoots list is accepted and deduped with no count or byte/token limit before it becomes the active environment's workspace roots and is rendered into the model-visible environment context. A large list can therefore inject unbounded context and churn the prompt cache; reject or truncate the per-environment roots at a hard cap before storing them.

AGENTS.md reference: AGENTS.md:L97-L100

Useful? React with 👍 / 👎.

let root = root.to_inferred_path_uri().ok_or_else(|| {
invalid_request(format!(
"invalid runtime workspace root for environment `{environment_id}`: path `{root}` does not use absolute POSIX or Windows path syntax"
))
})?;
if !resolved_roots.contains(&root) {
resolved_roots.push(root);
}
}
Ok::<_, JSONRPCErrorError>(resolved_roots)
})
.transpose()?
.unwrap_or_else(|| vec![cwd.clone()]);
Comment thread
pakrym-oai marked this conversation as resolved.
selections.push(TurnEnvironmentSelection {
environment_id,
cwd,
workspace_roots,
});
}
thread_manager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,22 +619,21 @@ pub(super) async fn handle_pending_thread_resume_request(
}

let config_snapshot = pending.config_snapshot;
let sandbox = config_snapshot.sandbox_policy().into();
let cwd = config_snapshot.cwd().clone();
let ThreadConfigSnapshot {
model,
model_provider_id,
service_tier,
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
workspace_roots,
reasoning_effort,
originator,
..
} = config_snapshot;
let instruction_sources = pending.instruction_sources;
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
let active_permission_profile =
thread_response_active_permission_profile(active_permission_profile);
let session_id = conversation.session_configured().session_id.to_string();
Expand Down
28 changes: 9 additions & 19 deletions codex-rs/app-server/src/request_processors/thread_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,9 +968,9 @@ impl ThreadRequestProcessor {
"`permissions` cannot be combined with `sandbox`",
));
}
let environment_selections =
resolve_turn_environment_selections(self.thread_manager.as_ref(), environments)?;
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
let environments =
resolve_turn_environment_selections(self.thread_manager.as_ref(), environments)?;
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
Expand Down Expand Up @@ -1017,7 +1017,7 @@ impl ThreadRequestProcessor {
history_mode.map(Into::into),
session_start_source,
thread_source.map(Into::into),
environment_selections,
environments,
service_name,
allow_provider_model_fallback,
experimental_raw_events,
Expand Down Expand Up @@ -1094,7 +1094,7 @@ impl ThreadRequestProcessor {
history_mode: Option<ThreadHistoryMode>,
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
thread_source: Option<codex_protocol::protocol::ThreadSource>,
environments: Option<Vec<TurnEnvironmentSelection>>,
environment_selections: Option<Vec<TurnEnvironmentSelection>>,
service_name: Option<String>,
allow_provider_model_fallback: bool,
experimental_raw_events: bool,
Expand All @@ -1107,7 +1107,6 @@ impl ThreadRequestProcessor {
.load_with_overrides(config_overrides.clone(), typesafe_overrides.clone())
.await
.map_err(|err| config_load_error(&err))?;

// The user may have requested WorkspaceWrite or DangerFullAccess via
// the command line, though in the process of deriving the Config, it
// could be downgraded to ReadOnly (perhaps there is no sandbox
Expand Down Expand Up @@ -1188,10 +1187,10 @@ impl ThreadRequestProcessor {
}
}

let environments = environments.unwrap_or_else(|| {
let environments = environment_selections.unwrap_or_else(|| {
listener_task_context
.thread_manager
.default_environment_selections(&config.cwd)
.default_environment_selections(&config.cwd, &config.workspace_roots)
});
let dynamic_tools = dynamic_tools.unwrap_or_default();
if !dynamic_tools.is_empty() {
Expand Down Expand Up @@ -1316,10 +1315,7 @@ impl ThreadRequestProcessor {
/*has_in_progress_turn*/ false,
);

let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd().as_path(),
);
let sandbox = config_snapshot.sandbox_policy().into();
let cwd = config_snapshot.cwd().clone();
let active_permission_profile =
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
Expand Down Expand Up @@ -2893,10 +2889,7 @@ impl ThreadRequestProcessor {
/*has_live_in_progress_turn*/ false,
);
let config_snapshot = codex_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd().as_path(),
);
let sandbox = config_snapshot.sandbox_policy().into();
let active_permission_profile = thread_response_active_permission_profile(
config_snapshot.active_permission_profile,
);
Expand Down Expand Up @@ -3655,10 +3648,7 @@ impl ThreadRequestProcessor {
/*has_in_progress_turn*/ false,
);
let config_snapshot = forked_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd().as_path(),
);
let sandbox = config_snapshot.sandbox_policy().into();
let active_permission_profile =
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
let thread_originator = config_snapshot.originator.clone();
Expand Down
Loading
Loading