Compare commits

...
Author SHA1 Message Date
Miguel PalhasandClaude Opus 4.6 4727c7dc20 feat(maestro): auto-approve ACP permissions via preset config
Structured sessions ignored the --dangerously-skip-permissions flags in
preset commands because the daemon discards CLI flags when spawning the
ACP adapter binary. This adds an `auto_approve` bool to SpawnPreset and
CreateSessionRequest so the operator can declare it in config.toml:

  { id = "claude", command = "claude", auto_approve = true }

When set, the daemon's permission pump immediately responds AllowAlways
to every ACP permission request without surfacing it to clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-07 11:19:09 +01:00
21 changed files with 128 additions and 2 deletions
+1
View File
@@ -57,6 +57,7 @@ pub async fn run_spawn(client: &DaemonClient, args: SpawnArgs) -> Result<Session
env, env,
metadata, metadata,
worktree, worktree,
auto_approve: false,
}; };
client.create_session(&body).await client.create_session(&body).await
} }
@@ -28,6 +28,7 @@ async fn info_carries_version_and_presets() {
id: "claude".into(), id: "claude".into(),
label: "Claude".into(), label: "Claude".into(),
command: "claude".into(), command: "claude".into(),
auto_approve: false,
}], }],
..Default::default() ..Default::default()
}); });
@@ -97,6 +98,7 @@ async fn spawn_round_trips_through_the_daemon() {
env: Default::default(), env: Default::default(),
metadata: Default::default(), metadata: Default::default(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
+10
View File
@@ -313,16 +313,19 @@ pub fn builtin_presets() -> Vec<SpawnPreset> {
id: "claude".into(), id: "claude".into(),
label: "Claude".into(), label: "Claude".into(),
command: "claude".into(), command: "claude".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "opencode".into(), id: "opencode".into(),
label: "opencode".into(), label: "opencode".into(),
command: "opencode".into(), command: "opencode".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "zsh".into(), id: "zsh".into(),
label: "Shell".into(), label: "Shell".into(),
command: "zsh".into(), command: "zsh".into(),
auto_approve: false,
}, },
] ]
} }
@@ -334,6 +337,7 @@ pub fn custom_preset() -> SpawnPreset {
id: CUSTOM_PRESET_ID.into(), id: CUSTOM_PRESET_ID.into(),
label: "Custom…".into(), label: "Custom…".into(),
command: String::new(), command: String::new(),
auto_approve: false,
} }
} }
@@ -515,6 +519,7 @@ auto_install = false
id: "fish".into(), id: "fish".into(),
label: "fish".into(), label: "fish".into(),
command: "fish".into(), command: "fish".into(),
auto_approve: false,
}]), }]),
..Default::default() ..Default::default()
}; };
@@ -551,6 +556,7 @@ auto_install = false
id: " ".into(), id: " ".into(),
label: "x".into(), label: "x".into(),
command: "x".into(), command: "x".into(),
auto_approve: false,
}]), }]),
..Default::default() ..Default::default()
}; };
@@ -561,6 +567,7 @@ auto_install = false
id: "x".into(), id: "x".into(),
label: "x".into(), label: "x".into(),
command: "".into(), command: "".into(),
auto_approve: false,
}]), }]),
..Default::default() ..Default::default()
}; };
@@ -578,11 +585,13 @@ auto_install = false
id: "a".into(), id: "a".into(),
label: "a".into(), label: "a".into(),
command: "a".into(), command: "a".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "a".into(), id: "a".into(),
label: "a2".into(), label: "a2".into(),
command: "a2".into(), command: "a2".into(),
auto_approve: false,
}, },
]), ]),
..Default::default() ..Default::default()
@@ -597,6 +606,7 @@ auto_install = false
id: "custom".into(), id: "custom".into(),
label: "Custom".into(), label: "Custom".into(),
command: "whatever".into(), command: "whatever".into(),
auto_approve: false,
}]), }]),
..Default::default() ..Default::default()
}; };
+18 -2
View File
@@ -122,6 +122,9 @@ pub struct SessionRecord {
/// Completion signal for the task draining ACP notifications into the /// Completion signal for the task draining ACP notifications into the
/// event log. Shutdown waits for it before returning. /// event log. Shutdown waits for it before returning.
acp_event_pump: Option<watch::Receiver<bool>>, acp_event_pump: Option<watch::Receiver<bool>>,
/// When `true`, the permission pump auto-responds `AllowAlways` to every
/// ACP permission request without surfacing it to clients.
pub auto_approve: bool,
} }
impl SessionRecord { impl SessionRecord {
@@ -1414,7 +1417,7 @@ impl SessionManager {
let mut sessions = self.inner.sessions.lock().await; let mut sessions = self.inner.sessions.lock().await;
let event_pump = event_pump.map(|(acp, events)| { let event_pump = event_pump.map(|(acp, events)| {
let done = start_acp_event_pump(&self.inner, id, &acp, events.clone()); let done = start_acp_event_pump(&self.inner, id, &acp, events.clone());
start_acp_permission_pump(&self.inner, id, &acp, events); start_acp_permission_pump(&self.inner, id, &acp, events, req.auto_approve);
done done
}); });
sessions.insert( sessions.insert(
@@ -1435,6 +1438,7 @@ impl SessionManager {
acp_events, acp_events,
structured_activity_watermark: None, structured_activity_watermark: None,
acp_event_pump: event_pump, acp_event_pump: event_pump,
auto_approve: req.auto_approve,
}, },
); );
} }
@@ -1647,6 +1651,7 @@ impl SessionManager {
acp_events: None, acp_events: None,
structured_activity_watermark: None, structured_activity_watermark: None,
acp_event_pump: None, acp_event_pump: None,
auto_approve: false,
}, },
); );
let _ = self let _ = self
@@ -1824,11 +1829,14 @@ impl SessionManager {
acp_events, acp_events,
structured_activity_watermark: None, structured_activity_watermark: None,
acp_event_pump: None, acp_event_pump: None,
// Recovery cannot know the original preset's flag; the
// operator re-enables it by restarting with the preset.
auto_approve: false,
}, },
); );
if let Some((acp, events)) = event_pump { if let Some((acp, events)) = event_pump {
let done = start_acp_event_pump(&self.inner, id, &acp, events.clone()); let done = start_acp_event_pump(&self.inner, id, &acp, events.clone());
start_acp_permission_pump(&self.inner, id, &acp, events); start_acp_permission_pump(&self.inner, id, &acp, events, false);
if let Some(record) = self.inner.sessions.lock().await.get_mut(&id) { if let Some(record) = self.inner.sessions.lock().await.get_mut(&id) {
record.acp_event_pump = Some(done); record.acp_event_pump = Some(done);
} }
@@ -2327,6 +2335,7 @@ fn start_acp_permission_pump(
id: Uuid, id: Uuid,
acp: &Arc<AcpSession>, acp: &Arc<AcpSession>,
events: Arc<Mutex<AcpEventStream>>, events: Arc<Mutex<AcpEventStream>>,
auto_approve: bool,
) { ) {
let Some(mut requests) = acp.take_permission_requests() else { let Some(mut requests) = acp.take_permission_requests() else {
return; return;
@@ -2340,6 +2349,12 @@ fn start_acp_permission_pump(
response, response,
}) = requests.recv().await }) = requests.recv().await
{ {
if auto_approve {
let outcome =
permission_outcome(ApprovalDecision::AllowForSession, &request.options);
let _ = response.send(outcome);
continue;
}
let mut events = events.lock().await; let mut events = events.lock().await;
if events.projection.active_turn() != Some(turn) { if events.projection.active_turn() != Some(turn) {
let _ = response.send(maestro_acp::schema::RequestPermissionOutcome::Cancelled); let _ = response.send(maestro_acp::schema::RequestPermissionOutcome::Cancelled);
@@ -3060,6 +3075,7 @@ mod tests {
env: std::collections::HashMap::new(), env: std::collections::HashMap::new(),
metadata: std::collections::HashMap::new(), metadata: std::collections::HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
} }
} }
+3
View File
@@ -112,6 +112,7 @@ fn structured_command(
env, env,
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
} }
} }
@@ -705,6 +706,7 @@ async fn shutdown_preserves_a_pristine_structured_worktree_for_recovery() {
branch: branch.into(), branch: branch.into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1187,6 +1189,7 @@ async fn cancelling_a_structured_handshake_rolls_back_and_settles_admission() {
branch: "cancelled-create".into(), branch: "cancelled-create".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}; };
let create = { let create = {
@@ -48,6 +48,7 @@ async fn spawn_sleeping(manager: &SessionManager) -> Uuid {
env: Default::default(), env: Default::default(),
metadata: Default::default(), metadata: Default::default(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap() .unwrap()
+1
View File
@@ -81,6 +81,7 @@ async fn create_bash_session(h: &Harness) -> Uuid {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
+1
View File
@@ -56,6 +56,7 @@ async fn spawn(manager: &SessionManager, name: Option<&str>) -> Uuid {
env: Default::default(), env: Default::default(),
metadata, metadata,
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap() .unwrap()
@@ -29,6 +29,7 @@ fn request(cwd: &std::path::Path, repo: &std::path::Path, branch: &str) -> Creat
branch: branch.into(), branch: branch.into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
} }
} }
+13
View File
@@ -162,6 +162,7 @@ async fn structured_session_with_env(h: &Harness, env: HashMap<String, String>)
env, env,
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap() .unwrap()
@@ -493,6 +494,7 @@ async fn structured_prompt_rejects_unknown_and_unavailable_sessions() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -626,6 +628,7 @@ async fn structured_replay_rejects_unknown_terminal_and_dead_sessions() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -768,6 +771,7 @@ async fn list_sessions_starts_empty_and_reflects_creates() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}; };
let created: Session = reqwest::Client::new() let created: Session = reqwest::Client::new()
.post(url(h.addr, "/api/sessions")) .post(url(h.addr, "/api/sessions"))
@@ -985,6 +989,7 @@ async fn patch_session_renames_it() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1028,6 +1033,7 @@ async fn post_focus_stamps_last_focused_at() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1062,6 +1068,7 @@ async fn delete_owned_session_kills_it() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1084,6 +1091,7 @@ async fn spawn_via_manager(h: &Harness) -> Uuid {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap() .unwrap()
@@ -1198,6 +1206,7 @@ async fn delete_a_session_whose_worktree_holds_unsaved_work_returns_409_with_the
branch: "feat/guarded".into(), branch: "feat/guarded".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1251,6 +1260,7 @@ async fn delete_with_force_ends_the_session_and_keeps_the_worktree() {
branch: "feat/guarded".into(), branch: "feat/guarded".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1281,6 +1291,7 @@ async fn attach_ws_streams_pty_bytes() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1383,6 +1394,7 @@ async fn events_ws_sends_a_snapshot_then_streams_deltas() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1495,6 +1507,7 @@ async fn every_events_subscriber_gets_its_own_snapshot() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
+1
View File
@@ -57,6 +57,7 @@ async fn spawn_named(manager: &SessionManager, name: Option<&str>) -> Uuid {
env: Default::default(), env: Default::default(),
metadata, metadata,
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap() .unwrap()
@@ -70,6 +70,7 @@ impl Fx {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
} }
} }
} }
@@ -28,6 +28,7 @@ async fn create_then_kill_round_trip() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -70,6 +71,7 @@ async fn create_appends_to_spawn_history() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::from([("name".into(), serde_json::json!("neon-drifter"))]), metadata: HashMap::from([("name".into(), serde_json::json!("neon-drifter"))]),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -100,6 +102,7 @@ async fn create_without_history_logs_nothing() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -123,6 +126,7 @@ async fn recover_adopts_existing_tmux_sessions() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -166,6 +170,7 @@ async fn create_captures_initial_output_in_replay_buffer() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -215,6 +220,7 @@ async fn attach_replay_is_the_current_screen_not_the_byte_tail() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -273,6 +279,7 @@ async fn attach_replay_carries_the_panes_mouse_modes() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -324,6 +331,7 @@ async fn recover_re_pipes_pane_so_output_reaches_subscribers() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -387,6 +395,7 @@ async fn pty_broadcast_delivers_bytes_to_subscribers() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.unwrap(); .unwrap();
@@ -438,6 +447,7 @@ async fn liveness_watcher_marks_dead_sessions_and_keeps_them_until_dismissed() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -508,6 +518,7 @@ async fn watcher_refreshes_the_blocked_work_indicator() {
branch: "feat/indicator".into(), branch: "feat/indicator".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -601,6 +612,7 @@ async fn kill_removes_worktree_even_when_spec_says_keep() {
branch: "feat/keep".into(), branch: "feat/keep".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -639,6 +651,7 @@ async fn killing_refuses_a_worktree_with_unsaved_work_and_keeps_the_session() {
branch: "feat/guarded".into(), branch: "feat/guarded".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -703,6 +716,7 @@ async fn killing_refuses_until_the_branch_is_pushed() {
branch: "feat/pushed".into(), branch: "feat/pushed".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -753,6 +767,7 @@ async fn force_killing_a_guarded_worktree_leaves_it_on_disk() {
branch: "feat/forced".into(), branch: "feat/forced".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -799,6 +814,7 @@ async fn force_on_a_clear_worktree_removes_it_as_usual() {
branch: "feat/clear".into(), branch: "feat/clear".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -840,6 +856,7 @@ async fn killing_an_ignored_only_worktree_succeeds() {
branch: "feat/rusty".into(), branch: "feat/rusty".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -882,6 +899,7 @@ async fn natural_exit_of_a_guarded_worktree_leaves_it_on_disk() {
branch: "feat/exited".into(), branch: "feat/exited".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -944,6 +962,7 @@ async fn natural_exit_of_a_pristine_worktree_removes_it() {
branch: "feat/clean".into(), branch: "feat/clean".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1001,6 +1020,7 @@ async fn liveness_watcher_leaves_running_sessions_alone() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1040,6 +1060,7 @@ async fn worktree_base_repo_records_the_main_checkout_not_the_request() {
branch: "feat/one".into(), branch: "feat/one".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create first"); .expect("create first");
@@ -1058,6 +1079,7 @@ async fn worktree_base_repo_records_the_main_checkout_not_the_request() {
branch: "feat/two".into(), branch: "feat/two".into(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
.await .await
.expect("create second"); .expect("create second");
@@ -1093,6 +1115,7 @@ async fn spawned_sessions_export_maestro_session_id_and_url() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1132,6 +1155,7 @@ async fn a_freshly_created_session_has_never_been_focused() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1158,6 +1182,7 @@ async fn set_focused_true_stamps_last_focused_at() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
@@ -1207,6 +1232,7 @@ async fn attaching_stamps_last_focused_at() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("create"); .expect("create");
+8
View File
@@ -33,6 +33,7 @@ fn create_req(cwd: &std::path::Path, command: &str) -> CreateSessionRequest {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
} }
} }
@@ -111,6 +112,7 @@ fn worktree_req(base: &std::path::Path, branch: &str, create_branch: bool) -> Cr
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(base, branch, create_branch)), worktree: Some(worktree_spec(base, branch, create_branch)),
auto_approve: false,
} }
} }
@@ -206,6 +208,7 @@ async fn kill_removes_clean_worktree() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(base.path(), "feature/e2e", true)), worktree: Some(worktree_spec(base.path(), "feature/e2e", true)),
auto_approve: false,
}; };
let s = client.spawn_session(&req).await.expect("spawn worktree"); let s = client.spawn_session(&req).await.expect("spawn worktree");
@@ -260,6 +263,7 @@ async fn kill_refuses_worktree_with_uncommitted_changes() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(base.path(), "feature/unsaved", true)), worktree: Some(worktree_spec(base.path(), "feature/unsaved", true)),
auto_approve: false,
}; };
let s = client.spawn_session(&req).await.expect("spawn worktree"); let s = client.spawn_session(&req).await.expect("spawn worktree");
@@ -319,6 +323,7 @@ async fn spawn_on_existing_branch_checks_it_out() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(base.path(), "feature/existing", false)), worktree: Some(worktree_spec(base.path(), "feature/existing", false)),
auto_approve: false,
}; };
let s = client let s = client
@@ -827,6 +832,7 @@ async fn natural_exit_settles_dirty_and_pristine_worktrees_differently() {
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(dirty_repo.path(), "feature/messy", true)), worktree: Some(worktree_spec(dirty_repo.path(), "feature/messy", true)),
auto_approve: false,
}) })
.await .await
.expect("spawn dirty"); .expect("spawn dirty");
@@ -876,6 +882,7 @@ fn create_req_cmd(cwd: &std::path::Path, command: &str, branch: &str) -> CreateS
env: HashMap::new(), env: HashMap::new(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: Some(worktree_spec(cwd, branch, true)), worktree: Some(worktree_spec(cwd, branch, true)),
auto_approve: false,
} }
} }
@@ -1068,6 +1075,7 @@ async fn sigterm_stops_structured_child_before_events_ws_drains() {
env: fake.env.clone(), env: fake.env.clone(),
metadata: HashMap::new(), metadata: HashMap::new(),
worktree: None, worktree: None,
auto_approve: false,
}) })
.await .await
.expect("spawn structured session"); .expect("spawn structured session");
+10
View File
@@ -383,6 +383,10 @@ pub struct CreateSessionRequest {
/// untouched. /// untouched.
#[serde(default)] #[serde(default)]
pub view: SessionView, pub view: SessionView,
/// When `true`, the daemon auto-approves every ACP permission request
/// for this structured session.
#[serde(default)]
pub auto_approve: bool,
} }
/// Result of `GET /api/dirs`: the immediate subdirectories of one directory. /// Result of `GET /api/dirs`: the immediate subdirectories of one directory.
@@ -522,6 +526,11 @@ pub struct SpawnPreset {
pub id: String, pub id: String,
pub label: String, pub label: String,
pub command: String, pub command: String,
/// When `true`, the daemon auto-approves every ACP permission request
/// for structured sessions spawned from this preset instead of surfacing
/// them to the client.
#[serde(default)]
pub auto_approve: bool,
} }
/// Build provenance of the running daemon: the commit it was compiled /// Build provenance of the running daemon: the commit it was compiled
@@ -1251,6 +1260,7 @@ mod tests {
create_branch: true, create_branch: true,
}), }),
view: SessionView::Terminal, view: SessionView::Terminal,
auto_approve: false,
}; };
assert_eq!(req, round_trip(&req)); assert_eq!(req, round_trip(&req));
} }
+2
View File
@@ -3009,6 +3009,7 @@ mod tests {
env: Default::default(), env: Default::default(),
metadata: Default::default(), metadata: Default::default(),
worktree: None, worktree: None,
auto_approve: false,
}; };
app.submit_spawn(req).await; app.submit_spawn(req).await;
@@ -3057,6 +3058,7 @@ mod tests {
env: Default::default(), env: Default::default(),
metadata: Default::default(), metadata: Default::default(),
worktree: None, worktree: None,
auto_approve: false,
}; };
let structured_id = Uuid::from_u128(188); let structured_id = Uuid::from_u128(188);
+15
View File
@@ -718,6 +718,10 @@ impl DraftPane {
); );
} }
let worktree = self.build_worktree()?; let worktree = self.build_worktree()?;
let auto_approve = self
.presets
.get(self.selected_preset)
.is_some_and(|p| p.auto_approve);
Ok(CreateSessionRequest { Ok(CreateSessionRequest {
view: if self.structured_view && self.supports_structured_view() { view: if self.structured_view && self.supports_structured_view() {
SessionView::Structured SessionView::Structured
@@ -729,6 +733,7 @@ impl DraftPane {
env: Default::default(), env: Default::default(),
metadata, metadata,
worktree, worktree,
auto_approve,
}) })
} }
@@ -1776,6 +1781,7 @@ mod tests {
id: "aider".into(), id: "aider".into(),
label: "Aider".into(), label: "Aider".into(),
command: "aider".into(), command: "aider".into(),
auto_approve: false,
}]); }]);
assert_eq!( assert_eq!(
draft.command(), draft.command(),
@@ -1798,11 +1804,13 @@ mod tests {
id: "claude".into(), id: "claude".into(),
label: "Claude".into(), label: "Claude".into(),
command: "claude".into(), command: "claude".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "aider".into(), id: "aider".into(),
label: "Aider".into(), label: "Aider".into(),
command: "aider".into(), command: "aider".into(),
auto_approve: false,
}, },
]); ]);
assert_eq!(draft.command(), "aider"); assert_eq!(draft.command(), "aider");
@@ -1822,6 +1830,7 @@ mod tests {
id: "claude".into(), id: "claude".into(),
label: "Claude".into(), label: "Claude".into(),
command: "claude".into(), command: "claude".into(),
auto_approve: false,
}]); }]);
assert!(draft.is_custom()); assert!(draft.is_custom());
assert_eq!(draft.command(), "bash -l"); assert_eq!(draft.command(), "bash -l");
@@ -1898,16 +1907,19 @@ mod tests {
id: "claude".into(), id: "claude".into(),
label: "Claude".into(), label: "Claude".into(),
command: "claude".into(), command: "claude".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "codex".into(), id: "codex".into(),
label: "Codex".into(), label: "Codex".into(),
command: "codex".into(), command: "codex".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "opencode".into(), id: "opencode".into(),
label: "OpenCode".into(), label: "OpenCode".into(),
command: "opencode".into(), command: "opencode".into(),
auto_approve: false,
}, },
]); ]);
assert!(!draft.structured_view); assert!(!draft.structured_view);
@@ -2046,16 +2058,19 @@ mod tests {
id: "claude-wrapped".into(), id: "claude-wrapped".into(),
label: "Claude (npx)".into(), label: "Claude (npx)".into(),
command: "npx claude-code --resume".into(), command: "npx claude-code --resume".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "codex-wrapped".into(), id: "codex-wrapped".into(),
label: "Codex (npx)".into(), label: "Codex (npx)".into(),
command: "npx codex exec".into(), command: "npx codex exec".into(),
auto_approve: false,
}, },
SpawnPreset { SpawnPreset {
id: "aider".into(), id: "aider".into(),
label: "Aider".into(), label: "Aider".into(),
command: "aider".into(), command: "aider".into(),
auto_approve: false,
}, },
]); ]);
+2
View File
@@ -121,6 +121,7 @@ impl FastForwardDefaults {
branch: handle.to_string(), branch: handle.to_string(),
create_branch: true, create_branch: true,
}), }),
auto_approve: false,
}) })
} }
@@ -184,6 +185,7 @@ mod tests {
id: id.into(), id: id.into(),
label: id.into(), label: id.into(),
command: command.into(), command: command.into(),
auto_approve: false,
} }
} }
+1
View File
@@ -7,6 +7,7 @@ const base: SpawnFormValues = {
cwd: "/home/user/project", cwd: "/home/user/project",
name: "amber-fox", name: "amber-fox",
structuredView: false, structuredView: false,
autoApprove: false,
worktreeEnabled: false, worktreeEnabled: false,
worktreeBranch: "", worktreeBranch: "",
worktreeBranchMode: "new", worktreeBranchMode: "new",
+9
View File
@@ -49,6 +49,7 @@ export interface SpawnFormValues {
cwd: string; cwd: string;
name: string; name: string;
structuredView: boolean; structuredView: boolean;
autoApprove: boolean;
worktreeEnabled: boolean; worktreeEnabled: boolean;
worktreeBranch: string; worktreeBranch: string;
worktreeBranchEdited: boolean; worktreeBranchEdited: boolean;
@@ -79,6 +80,10 @@ export function buildSpawnRequest(
req.view = "Structured"; req.view = "Structured";
} }
if (values.autoApprove) {
req.auto_approve = true;
}
if (values.worktreeEnabled) { if (values.worktreeEnabled) {
const mode = values.worktreeBranchMode ?? "new"; const mode = values.worktreeBranchMode ?? "new";
const branch = values.worktreeBranch.trim(); const branch = values.worktreeBranch.trim();
@@ -261,6 +266,8 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
let ready = false; let ready = false;
let destroyed = false; let destroyed = false;
const branchValue = () => (branchMode === "existing" ? branchSelect.value : branchInput.value); const branchValue = () => (branchMode === "existing" ? branchSelect.value : branchInput.value);
const selectedAutoApprove = () =>
presets.find((p) => p.id === selectedPresetId)?.auto_approve ?? false;
const emit = () => { const emit = () => {
if (!ready || destroyed) return; if (!ready || destroyed) return;
opts.onChange?.({ opts.onChange?.({
@@ -268,6 +275,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
cwd, cwd,
name: nameInput.value, name: nameInput.value,
structuredView: structuredView.checked, structuredView: structuredView.checked,
autoApprove: selectedAutoApprove(),
worktreeEnabled: wtEnabled.checked, worktreeEnabled: wtEnabled.checked,
worktreeBranch: branchValue(), worktreeBranch: branchValue(),
worktreeBranchEdited: branchEdited, worktreeBranchEdited: branchEdited,
@@ -917,6 +925,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
cwd, cwd,
name: nameInput.value, name: nameInput.value,
structuredView: structuredView.checked, structuredView: structuredView.checked,
autoApprove: selectedAutoApprove(),
worktreeEnabled: wtEnabled.checked, worktreeEnabled: wtEnabled.checked,
worktreeBranch: branchValue(), worktreeBranch: branchValue(),
worktreeBranchEdited: branchEdited, worktreeBranchEdited: branchEdited,
+2
View File
@@ -91,6 +91,7 @@ export interface SpawnPreset {
id: string; id: string;
label: string; label: string;
command: string; command: string;
auto_approve?: boolean;
} }
/** `GET /api/info` — the daemon's identity plus its spawn presets (#26). /** `GET /api/info` — the daemon's identity plus its spawn presets (#26).
@@ -277,6 +278,7 @@ export interface CreateSessionRequest {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
worktree?: WorktreeSpec; worktree?: WorktreeSpec;
view?: SessionView; view?: SessionView;
auto_approve?: boolean;
} }
export type CatalogEvent = export type CatalogEvent =