Support managed worktrees for interactive sessions and forks (#43069)

## Why

`--worktree` previously supported only `codex exec` and `codex exec fork`. Interactive sessions need the same managed checkout support, with configuration and policy resolved for the destination before starting a turn.

## What changed

- Enable `codex --worktree` and `codex fork --worktree <session-id>` behind the `worktrees` feature, restricted to local sessions. Interactive forks require an explicit session selector.
- Resolve interactive and exec worktree forks from the session's latest saved working directory unless `--cd` is supplied. Keep relative `--add-dir` paths anchored to the invocation directory.
- Load interactive destination configuration before telemetry and login policy initialization, and bind checkout ownership before the first turn.
- Reject explicitly untrusted sources, including when destination cloud policy or refreshed configuration reveals distrust. Retain unbound interactive checkouts after startup failure and report manual recovery instructions.

## Testing

Add CLI integration and TUI tests for startup and named forks, destination instructions and configuration, ownership before the first request, trust enforcement, and retained-checkout recovery. Extend exec coverage for saved fork directories, explicit `--cd`, relative writable roots, and cloud policy rejection.

GitOrigin-RevId: 371583f7feca73b218a4ff188b2b85812c8439e6
This commit is contained in:
Benjamin Carlsson
2026-09-05 18:06:26 +00:00
committed by copyberry
parent 52e73e3a54
commit 3525845978
15 changed files with 1626 additions and 41 deletions
+28 -12
View File
@@ -2486,6 +2486,11 @@ fn reject_unsupported_worktree_for_subcommand(
}
match subcommand {
None => Ok(()),
Some(Subcommand::Fork(command)) if command.session_id.is_some() && !command.last => Ok(()),
Some(Subcommand::Fork(_)) => {
anyhow::bail!("`codex fork --worktree` requires an explicit session ID")
}
Some(Subcommand::Exec(command)) => match &command.command {
None | Some(ExecCommand::Fork(_)) => Ok(()),
Some(ExecCommand::Resume(_)) => anyhow::bail!(
@@ -2496,7 +2501,9 @@ fn reject_unsupported_worktree_for_subcommand(
}
},
_ => {
anyhow::bail!("`--worktree` currently supports only `codex exec` and `codex exec fork`")
anyhow::bail!(
"`--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`"
)
}
}
}
@@ -3317,10 +3324,18 @@ mod tests {
}
#[test]
fn exec_worktree_flag_supports_root_local_and_nested_fork_positions() {
fn worktree_flag_supports_interactive_exec_and_explicit_fork_positions() {
let arguments = [
vec!["codex", "--worktree"],
vec!["codex", "--worktree", "hello"],
vec!["codex", "--worktree", "exec", "hello"],
vec!["codex", "exec", "--worktree", "hello"],
vec![
"codex",
"fork",
"--worktree",
"019f1234-5678-7000-8000-000000000001",
],
vec![
"codex",
"exec",
@@ -3338,7 +3353,7 @@ mod tests {
&cli.subcommand,
)
.is_ok(),
"headless worktree command should be accepted: {arguments:?}",
"supported worktree command should be accepted: {arguments:?}",
);
}
}
@@ -3346,8 +3361,9 @@ mod tests {
#[test]
fn worktree_flag_rejects_unsupported_session_and_management_commands() {
let arguments = [
vec!["codex", "--worktree"],
vec!["codex", "--worktree", "login"],
vec!["codex", "fork", "--worktree"],
vec!["codex", "fork", "--worktree", "--last"],
vec!["codex", "exec", "resume", "--worktree", "session"],
vec!["codex", "exec", "review", "--worktree"],
vec!["codex", "resume", "--worktree", "session"],
@@ -3363,17 +3379,17 @@ mod tests {
],
];
let mut errors = Vec::new();
for arguments in arguments {
let cli = MultitoolCli::try_parse_from(&arguments).expect("parse shared worktree flag");
assert!(
reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.is_err(),
"unsupported command must be rejected: {arguments:?}",
);
let error = reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.expect_err("unsupported worktree command");
errors.push(format!("{}: {error}", arguments.join(" ")));
}
insta::assert_snapshot!("unsupported_worktree_commands", errors.join("\n"));
}
#[test]
@@ -0,0 +1,13 @@
---
source: cli/src/main.rs
assertion_line: 3324
expression: "errors.join(\"\\n\")"
---
codex --worktree login: `--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`
codex fork --worktree: `codex fork --worktree` requires an explicit session ID
codex fork --worktree --last: `codex fork --worktree` requires an explicit session ID
codex exec resume --worktree session: `--worktree` cannot resume an existing session; use `codex exec fork --worktree`
codex exec review --worktree: `--worktree` is not supported for code review
codex resume --worktree session: `--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`
codex archive session --worktree: `--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`
codex queue --thread session --message hi --worktree: `--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`