Compare commits

...

7 Commits

Author SHA1 Message Date
Eva Wong
351d4995ef fix(core): initialize sample project roots 2026-05-05 12:08:20 -07:00
Eva Wong
356a3e2d6c fix(exec): store project roots in config 2026-05-05 11:59:54 -07:00
Eva Wong
75a2bd95b0 fix(exec): use platform sandbox expectation 2026-05-05 11:14:23 -07:00
Eva Wong
7cd6a78ddf fix(exec): annotate project roots test args 2026-05-05 10:43:17 -07:00
Eva Wong
ac0e04c50b fix(exec): satisfy clippy for project roots 2026-05-05 10:34:16 -07:00
Eva Wong
acbb2f1222 chore(protocol): stabilize thread project roots 2026-05-05 10:23:25 -07:00
Eva Wong
d9024bcffb fix(exec): carry add-dir roots through app server 2026-05-05 08:43:47 -07:00
19 changed files with 142 additions and 20 deletions

View File

@@ -3940,6 +3940,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -4123,6 +4132,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{

View File

@@ -6040,4 +6040,4 @@
}
],
"title": "ServerNotification"
}
}

View File

@@ -16877,6 +16877,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -17171,6 +17180,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -18458,4 +18476,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}

View File

@@ -14763,6 +14763,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -15057,6 +15066,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -16343,4 +16361,4 @@
},
"title": "CodexAppServerProtocolV2",
"type": "object"
}
}

View File

@@ -1393,4 +1393,4 @@
],
"title": "ItemCompletedNotification",
"type": "object"
}
}

View File

@@ -1393,4 +1393,4 @@
],
"title": "ItemStartedNotification",
"type": "object"
}
}

View File

@@ -1090,6 +1090,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{

View File

@@ -270,6 +270,15 @@
}
]
},
"projectRoots": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"sandbox": {
"anyOf": [
{

View File

@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { Personality } from "../Personality";
import type { ServiceTier } from "../ServiceTier";
import type { JsonValue } from "../serde_json/JsonValue";
@@ -22,7 +23,7 @@ import type { SandboxMode } from "./SandboxMode";
export type ThreadResumeParams = {threadId: string, /**
* Configuration overrides for the resumed thread, if any.
*/
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, projectRoots?: Array<AbsolutePathBuf> | null, approvalPolicy?: AskForApproval | null, /**
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/

View File

@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { Personality } from "../Personality";
import type { ServiceTier } from "../ServiceTier";
import type { JsonValue } from "../serde_json/JsonValue";
@@ -9,7 +10,7 @@ import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
import type { ThreadStartSource } from "./ThreadStartSource";
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, projectRoots?: Array<AbsolutePathBuf> | null, approvalPolicy?: AskForApproval | null, /**
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/

View File

@@ -3790,6 +3790,8 @@ pub struct ThreadStartParams {
pub service_tier: Option<Option<ServiceTier>>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub project_roots: Option<Vec<AbsolutePathBuf>>,
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -3944,6 +3946,8 @@ pub struct ThreadResumeParams {
pub service_tier: Option<Option<ServiceTier>>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub project_roots: Option<Vec<AbsolutePathBuf>>,
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,

View File

@@ -728,6 +728,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
project_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -756,6 +757,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
project_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -1107,6 +1109,7 @@ impl ThreadRequestProcessor {
model_provider: Option<String>,
service_tier: Option<Option<codex_protocol::config_types::ServiceTier>>,
cwd: Option<String>,
project_roots: Option<Vec<AbsolutePathBuf>>,
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
@@ -1115,16 +1118,25 @@ impl ThreadRequestProcessor {
developer_instructions: Option<String>,
personality: Option<Personality>,
) -> ConfigOverrides {
let cwd = cwd.map(PathBuf::from);
let cwd_for_filter = cwd.as_deref();
let additional_writable_roots = project_roots
.unwrap_or_default()
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.filter(|root| Some(root.as_path()) != cwd_for_filter)
.collect();
let mut overrides = ConfigOverrides {
model,
model_provider,
service_tier,
cwd: cwd.map(PathBuf::from),
cwd,
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
approvals_reviewer: approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core),
sandbox_mode: sandbox.map(SandboxMode::to_core),
additional_writable_roots,
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
@@ -2227,6 +2239,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
project_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -2262,6 +2275,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
project_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -2879,6 +2893,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
/*project_roots*/ None,
approval_policy,
approvals_reviewer,
sandbox,

View File

@@ -514,6 +514,7 @@ mod thread_processor_behavior_tests {
model_provider: None,
service_tier: Some(Some(codex_protocol::config_types::ServiceTier::Fast)),
cwd: None,
project_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox: None,

View File

@@ -664,6 +664,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
model_provider: None,
service_tier: None,
cwd: None,
project_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox: None,

View File

@@ -2854,7 +2854,7 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
std::fs::create_dir_all(&backend)?;
let overrides = ConfigOverrides {
cwd: Some(frontend),
cwd: Some(frontend.clone()),
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
additional_writable_roots: vec![PathBuf::from("../backend"), backend.clone()],
..Default::default()
@@ -2868,6 +2868,14 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
.await?;
let expected_backend = backend.abs();
assert_eq!(
config.project_roots,
vec![
frontend.abs(),
expected_backend.clone(),
expected_backend.clone()
]
);
if cfg!(target_os = "windows") {
match &config.legacy_sandbox_policy() {
SandboxPolicy::ReadOnly { .. } => {}
@@ -6366,6 +6374,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
project_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -6568,6 +6577,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
project_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -6724,6 +6734,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
project_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -6865,6 +6876,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
project_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(

View File

@@ -561,6 +561,11 @@ pub struct Config {
/// layer are resolved against this path.
pub cwd: AbsolutePathBuf,
/// Absolute project roots for the session. This is the cwd plus any
/// user-requested additional roots, before implicit writable roots are
/// added to the effective permission profile.
pub project_roots: Vec<AbsolutePathBuf>,
/// Preferred store for CLI auth credentials.
/// file (default): Use a file in the Codex home directory.
/// keyring: Use an OS-specific keyring service.
@@ -2950,6 +2955,9 @@ impl Config {
.value
.set(effective_permission_profile)
.map_err(std::io::Error::from)?;
let project_roots = std::iter::once(resolved_cwd.clone())
.chain(requested_additional_writable_roots.iter().cloned())
.collect::<Vec<_>>();
let config = Self {
model,
service_tier,
@@ -2959,6 +2967,7 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
project_roots,
startup_warnings,
permissions: Permissions {
approval_policy: constrained_approval_policy.value,

View File

@@ -304,7 +304,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
}
None => AbsolutePathBuf::current_dir()?,
};
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
let codex_home = match find_codex_home() {
@@ -945,6 +944,7 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: sandbox.flatten(),
permissions,
project_roots: Some(config.project_roots.clone()),
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
..ThreadStartParams::default()
@@ -968,6 +968,7 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: sandbox.flatten(),
permissions,
project_roots: Some(config.project_roots.clone()),
config: config_request_overrides_from_config(config),
..ThreadResumeParams::default()
}

View File

@@ -395,31 +395,35 @@ async fn thread_start_params_include_review_policy_when_auto_review_is_enabled()
async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let extra = AbsolutePathBuf::try_from(cwd.path().join("extra")).expect("absolute path");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
sandbox_mode: Some(SandboxMode::DangerFullAccess),
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
additional_writable_roots: vec![extra.clone().into_path_buf()],
..Default::default()
})
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("build config with legacy sandbox override");
let project_roots = vec![config.cwd.clone(), extra];
let expected_sandbox = sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
);
let start_params = thread_start_params_from_config(&config);
let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string());
assert_eq!(config.permissions.active_permission_profile(), None);
assert_eq!(
start_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
);
assert_eq!(config.project_roots, project_roots);
assert_eq!(start_params.sandbox, expected_sandbox);
assert_eq!(start_params.permissions, None);
assert_eq!(
resume_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
);
assert_eq!(start_params.project_roots, Some(project_roots.clone()));
assert_eq!(resume_params.sandbox, expected_sandbox);
assert_eq!(resume_params.permissions, None);
assert_eq!(resume_params.project_roots, Some(project_roots));
}
#[tokio::test]

View File

@@ -200,7 +200,8 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
tui_keymap: TuiKeymap::default(),
tui_vim_mode_default: false,
cwd,
cwd: cwd.clone(),
project_roots: vec![cwd],
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::File,