app-server: keep workspace roots environment-local

This commit is contained in:
pakrym-oai
2026-07-09 19:39:43 -07:00
parent 2bb2cd25c6
commit 6e417ed780
12 changed files with 43 additions and 43 deletions
+1 -1
View File
@@ -4498,7 +4498,7 @@
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
@@ -20172,7 +20172,7 @@
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.",
"items": {
"$ref": "#/definitions/v2/LegacyAppPathString"
},
@@ -17951,7 +17951,7 @@
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
@@ -283,7 +283,7 @@
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
@@ -364,7 +364,7 @@
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
@@ -5,7 +5,6 @@ import type { LegacyAppPathString } from "../LegacyAppPathString";
export type TurnEnvironmentParams = { environmentId: string, cwd: LegacyAppPathString,
/**
* Environment-native runtime workspace roots. Omitted inherits the
* request's top-level `runtimeWorkspaceRoots` fallback.
* Environment-native runtime workspace roots. Omitted defaults to `cwd`.
*/
runtimeWorkspaceRoots?: Array<LegacyAppPathString> | null, };
@@ -41,8 +41,7 @@ pub enum TurnStatus {
pub struct TurnEnvironmentParams {
pub environment_id: String,
pub cwd: LegacyAppPathString,
/// Environment-native runtime workspace roots. Omitted inherits the
/// request's top-level `runtimeWorkspaceRoots` fallback.
/// Environment-native runtime workspace roots. Omitted defaults to `cwd`.
#[ts(optional = nullable)]
pub runtime_workspace_roots: Option<Vec<LegacyAppPathString>>,
}
+1 -1
View File
@@ -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` supplies the default 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` and optional environment-native `runtimeWorkspaceRoots`. Omitted per-environment roots inherit the top-level fallback; an empty list explicitly selects no roots for that environment. 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.
@@ -464,7 +464,6 @@ use codex_thread_store::ThreadSortKey as StoreThreadSortKey;
use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use std::collections::BTreeMap;
use std::collections::HashMap;
@@ -564,7 +563,6 @@ fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>,
fn resolve_turn_environment_selections(
thread_manager: &ThreadManager,
environments: Option<Vec<TurnEnvironmentParams>>,
fallback_workspace_roots: &[AbsolutePathBuf],
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
let Some(environments) = environments else {
return Ok(None);
@@ -598,12 +596,7 @@ fn resolve_turn_environment_selections(
Ok::<_, JSONRPCErrorError>(resolved_roots)
})
.transpose()?
.unwrap_or_else(|| {
fallback_workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect()
});
.unwrap_or_else(|| vec![cwd.clone()]);
selections.push(TurnEnvironmentSelection {
environment_id,
cwd,
@@ -1189,7 +1189,6 @@ impl ThreadRequestProcessor {
let environments = resolve_turn_environment_selections(
listener_task_context.thread_manager.as_ref(),
environments,
&config.workspace_roots,
)?
.unwrap_or_else(|| {
listener_task_context
@@ -6,6 +6,7 @@ use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_path_uri::PathUri;
use crate::image_url::REMOTE_IMAGE_URL_ERROR;
use crate::image_url::is_remote_image_url;
@@ -484,15 +485,8 @@ impl TurnRequestProcessor {
let runtime_workspace_roots = params
.runtime_workspace_roots
.map(resolve_runtime_workspace_roots);
let fallback_workspace_roots = match runtime_workspace_roots.as_ref() {
Some(workspace_roots) => workspace_roots.clone(),
None => thread.config_snapshot().await.workspace_roots,
};
let environment_selections = resolve_turn_environment_selections(
self.thread_manager.as_ref(),
params.environments,
&fallback_workspace_roots,
)?;
let environment_selections =
resolve_turn_environment_selections(self.thread_manager.as_ref(), params.environments)?;
// Map v2 input items to core input items.
let mapped_items: Vec<CoreInputItem> = params
@@ -595,12 +589,10 @@ impl TurnRequestProcessor {
}
let snapshot = thread.config_snapshot().await;
let legacy_fallback_cwd = cwd.unwrap_or_else(|| snapshot.cwd().clone());
let legacy_fallback_workspace_roots = workspace_roots.unwrap_or(snapshot.workspace_roots);
let environment_selections = environment_selections.unwrap_or_else(|| {
self.thread_manager.default_environment_selections(
&legacy_fallback_cwd,
&legacy_fallback_workspace_roots,
)
let workspace_roots = workspace_roots.unwrap_or(snapshot.workspace_roots);
self.thread_manager
.default_environment_selections(&legacy_fallback_cwd, &workspace_roots)
});
Some(TurnEnvironmentSelections::new(
legacy_fallback_cwd,
@@ -674,15 +666,29 @@ impl TurnRequestProcessor {
"{method} permission selection missing thread snapshot"
)));
};
let workspace_roots = environments
.as_ref()
.map(|environments| {
environments
.environments
.first()
.map(|environment| {
environment
.workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()
.unwrap_or_default()
})
.unwrap_or_default()
})
.or_else(|| runtime_workspace_roots.clone())
.unwrap_or_else(|| snapshot.workspace_roots.clone());
let overrides = ConfigOverrides {
cwd: environments
.as_ref()
.map(|environments| environments.legacy_fallback_cwd.to_path_buf()),
workspace_roots: Some(
runtime_workspace_roots
.clone()
.unwrap_or_else(|| snapshot.workspace_roots.clone()),
),
workspace_roots: Some(workspace_roots),
default_permissions: Some(permissions),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
@@ -2712,7 +2712,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
#[cfg(unix)]
#[tokio::test]
async fn turn_environment_workspace_roots_override_and_inherit_turn_fallback() -> Result<()> {
async fn turn_environment_workspace_roots_override_and_default_to_cwd() -> Result<()> {
skip_if_no_network!(Ok(()));
let tmp = TempDir::new()?;
@@ -2801,7 +2801,7 @@ stream_max_retries = 0
text: "select dev profile".to_string(),
text_elements: Vec::new(),
}],
runtime_workspace_roots: Some(vec![fallback_root]),
runtime_workspace_roots: Some(vec![fallback_root.clone()]),
environments: Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: environment_cwd.clone().into(),
@@ -2833,7 +2833,7 @@ stream_max_retries = 0
runtime_workspace_roots: Some(vec![new_root]),
environments: Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: environment_cwd.into(),
cwd: fallback_root.into(),
runtime_workspace_roots: None,
}]),
..Default::default()
@@ -2870,10 +2870,14 @@ stream_max_retries = 0
);
let second_permissions = latest_permissions_instructions(&requests[1]);
assert!(second_permissions.contains(&new_root_text));
assert!(second_permissions.contains(&fallback_root_text));
assert!(
!second_permissions.contains(&old_root_text),
"omitted environment roots should inherit the updated turn-level fallback"
"omitted environment roots should not retain the previous environment roots"
);
assert!(
!second_permissions.contains(&new_root_text),
"explicit environments should ignore top-level roots"
);
Ok(())