mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Defer folder trust checks until starting a task from agents overview (#54048)
## Why The Agent Command Center does not open a workspace, so viewing it should not require consent for the launch directory. ## What changed Skip startup folder consent when opening the agents overview, while preserving initialization for the embedded fallback. Check folder trust before starting the first task, including through `/new` after dismissing the overview. On Windows, retain the sandbox setup prompt after folder trust is granted. ## Testing Add CLI tests for starting a task from an untrusted folder through the overview and through `/new` after dismissing it. A terminal snapshot verifies that the overview appears before folder consent. GitOrigin-RevId: 129be14a354b8abde2e8430c81599e2064ccac87
This commit is contained in:
@@ -20,6 +20,18 @@ async fn agents_can_offer_restart_for_managed_daemon() -> Result<()> {
|
||||
daemon_startup("agents").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn agents_defers_launch_directory_trust_until_starting_a_task() -> Result<()> {
|
||||
daemon_startup("agents-untrusted").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn agents_checks_trust_when_starting_after_dismissing_overview() -> Result<()> {
|
||||
daemon_startup("agents-untrusted-new").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn discovered_managed_daemon_can_restart_with_auto_start_disabled() -> Result<()> {
|
||||
@@ -173,13 +185,18 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
tui.disable_paste_burst = true\n\
|
||||
{}\n\
|
||||
notice.model_migrations.\"gpt-5.6-terra\" = \"gpt-6-sol\"\n\
|
||||
[projects.{}]\ntrust_level = \"trusted\"\n",
|
||||
[projects.{}]\ntrust_level = \"{}\"\n",
|
||||
if command == "discovered" {
|
||||
"tui.status_line=[\"model\",\"thread-id\",\"thread-name\"]"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
serde_json::to_string(&workspace_path)?,
|
||||
if command.starts_with("agents-untrusted") {
|
||||
"untrusted"
|
||||
} else {
|
||||
"trusted"
|
||||
},
|
||||
),
|
||||
)?;
|
||||
let mismatch = command.starts_with("mismatch-");
|
||||
@@ -216,7 +233,13 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
let mut steps: VecDeque<(&str, &[u8])> = VecDeque::new();
|
||||
if matches!(
|
||||
command,
|
||||
"start" | "agents" | "discovered" | "bedrock-running" | "restrictive-job"
|
||||
"start"
|
||||
| "agents"
|
||||
| "agents-untrusted"
|
||||
| "agents-untrusted-new"
|
||||
| "discovered"
|
||||
| "bedrock-running"
|
||||
| "restrictive-job"
|
||||
) || mismatch
|
||||
{
|
||||
// A selected package with a stopped daemon avoids installing a release.
|
||||
@@ -273,6 +296,15 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
steps.push_back(("GPT-5.6-Terra", b"/daemon\r"));
|
||||
steps.push_back(("Restartbackgroundserver", b"\x1b[F\r"));
|
||||
"Restartbackgroundserver?"
|
||||
} else if command.starts_with("agents-untrusted") {
|
||||
args.push("agents".into());
|
||||
if command == "agents-untrusted-new" {
|
||||
steps.push_back(("Notasksyet", b"\x1b"));
|
||||
steps.push_back(("GPT-5.6-Terra", b"/new\r"));
|
||||
} else {
|
||||
steps.push_back(("Notasksyet", b"n"));
|
||||
}
|
||||
"entercontinue·escback"
|
||||
} else if command == "discovered" {
|
||||
args.extend(["--disable".into(), "daemon_auto_start".into()]);
|
||||
// Empty drafts need not survive a daemon restart. Renaming materializes the
|
||||
@@ -382,6 +414,7 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
let writer = session.writer_sender();
|
||||
let mut stdout = spawned.stdout_rx;
|
||||
let mut output = String::new();
|
||||
let mut initial_agents_screen = String::new();
|
||||
let mut screen = vt100::Parser::new(
|
||||
/*rows*/ 40, /*cols*/ 120, /*scrollback_len*/ 0,
|
||||
);
|
||||
@@ -422,6 +455,12 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
if let Some((ready, input)) = steps.front()
|
||||
&& text.contains(ready)
|
||||
{
|
||||
if command == "agents-untrusted" && *ready == "Notasksyet" {
|
||||
if !text.contains("enteropennnew") {
|
||||
continue;
|
||||
}
|
||||
initial_agents_screen = screen.screen().contents();
|
||||
}
|
||||
if command == "discovered" {
|
||||
if *ready == "Restartbackgroundserver?" {
|
||||
existing_daemon = Some(fs::read(&pid_file)?);
|
||||
@@ -456,6 +495,22 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
steps.pop_front();
|
||||
output.clear();
|
||||
} else if steps.is_empty() && text.contains(expected) {
|
||||
if command == "agents-untrusted" {
|
||||
let transition = format!(
|
||||
"{initial_agents_screen}\n\n--- Start a task ---\n\n{}",
|
||||
screen.screen().contents()
|
||||
)
|
||||
.replace(&*workspace_path.to_string_lossy(), "[WORKSPACE]")
|
||||
.replace(&*home.path().to_string_lossy(), "[CODEX_HOME]")
|
||||
.replace(" tab/⇧tab filter", "tab/shift+tab filter");
|
||||
let transition = transition.lines().map(str::trim_end).collect::<Vec<_>>().join("\n");
|
||||
let snapshot = codex_utils_cargo_bin::find_resource!(
|
||||
"tests/snapshots/daemon_startup__agents_overview_before_folder_consent.snap"
|
||||
)?;
|
||||
insta::with_settings!({snapshot_path => snapshot.parent().context("snapshot has no parent")?}, {
|
||||
insta::assert_snapshot!("agents_overview_before_folder_consent", transition);
|
||||
});
|
||||
}
|
||||
if mismatch {
|
||||
let previous_pid = existing_daemon.as_ref().context("missing original daemon PID")?;
|
||||
ensure!((fs::read(&pid_file)? != *previous_pid) == restart);
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
source: cli/tests/daemon_startup.rs
|
||||
expression: transition
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
All 0 Needs you 0 Working 0 Ready 0 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
No tasks yet │
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
? help esc back ↑/↓ move enter open n new
|
||||
|
||||
--- Start a task ---
|
||||
|
||||
|
||||
Folder access
|
||||
[WORKSPACE]
|
||||
|
||||
Config, hooks, and exec policies from untrusted folders stay disabled. Trusted project folders can still contribute
|
||||
settings. Skills still load, and tools follow your permission settings. Opening will not change saved trust.
|
||||
|
||||
› 1. Open restricted
|
||||
2. Back to Agent Command Center
|
||||
|
||||
enter continue · esc back
|
||||
@@ -229,6 +229,10 @@ impl App {
|
||||
));
|
||||
AppRunControl::Continue
|
||||
})?;
|
||||
#[cfg(target_os = "windows")]
|
||||
if result.directory_trust_persisted && self.current_displayed_thread_id().is_none() {
|
||||
self.windows_sandbox.prompt_after_trust = true;
|
||||
}
|
||||
// Directory changes review hooks once, after attaching the destination.
|
||||
if result.directory_trust_persisted
|
||||
&& options.resumed_thread.is_none()
|
||||
|
||||
@@ -987,6 +987,28 @@ impl App {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Dismissing the initial agents overview can expose /new before any folder consent.
|
||||
// Resolve trust before loading defaults and applying explicit permission choices.
|
||||
if self.current_displayed_thread_id().is_none() {
|
||||
let mut config = self.chat_widget.config_ref().clone();
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
if let Err(control) = self
|
||||
.confirm_directory_trust(
|
||||
tui,
|
||||
app_server,
|
||||
&mut config,
|
||||
&cwd,
|
||||
crate::onboarding::DirectoryTrustOptions::default(),
|
||||
/*startup_draft*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if matches!(control, AppRunControl::Exit(_)) {
|
||||
self.app_event_tx.send(AppEvent::Exit(ExitMode::Immediate));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Start a fresh in-memory session while preserving resumability via persisted rollout
|
||||
// history. If an initial message is provided, `enqueue_primary_thread_session` suppresses it
|
||||
// until the new session is configured and any replayed turns have been rendered.
|
||||
|
||||
@@ -409,6 +409,11 @@ async fn replacement_preserves_remote_launch_paths_and_older_servers() -> Result
|
||||
home.path().join("config.toml"),
|
||||
"model = \"configured-model\"\n",
|
||||
)?;
|
||||
app.enqueue_primary_thread_session(
|
||||
test_thread_session(ThreadId::new(), app.config.cwd.to_path_buf()),
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
let (server, requests, proxy) = start_recording_app_server_with_history(
|
||||
&app.config,
|
||||
capabilities,
|
||||
|
||||
@@ -1848,9 +1848,14 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
|
||||
// Remote startup keeps its existing explicit --cd trust check. Resolving other
|
||||
// remote folders requires authoritative project-root information from the server.
|
||||
if !uses_remote_workspace || remote_cwd_override.is_some() {
|
||||
// The shared overview does not open a workspace; session selection checks trust when needed.
|
||||
// Embedded fallback cannot show the overview and still needs normal startup initialization.
|
||||
if (!matches!(
|
||||
session_selection,
|
||||
resume_picker::SessionSelection::AgentsOverview
|
||||
) || matches!(app_server_target, AppServerTarget::Embedded))
|
||||
&& (!uses_remote_workspace || remote_cwd_override.is_some())
|
||||
{
|
||||
let resumed_thread = if matches!(app_server_target, AppServerTarget::LocalDaemon { .. })
|
||||
&& let resume_picker::SessionSelection::Resume(target) = &session_selection
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user