fix: align ai agent permission modes

This commit is contained in:
lucaronin
2026-04-30 21:01:09 +02:00
parent 4a03f1675e
commit adcaa8a387
20 changed files with 200 additions and 68 deletions

View File

@@ -29,19 +29,13 @@ where
}
fn find_codex_binary() -> Result<PathBuf, String> {
if let Some(binary) = find_codex_binary_on_path() {
return Ok(binary);
}
if let Some(binary) = find_codex_binary_in_user_shell() {
return Ok(binary);
}
if let Some(binary) = find_existing_binary(codex_binary_candidates())? {
return Ok(binary);
}
Err("Codex CLI not found. Install it: https://developers.openai.com/codex/cli".into())
find_codex_binary_on_path()
.filter(is_usable_codex_binary)
.or_else(|| find_codex_binary_in_user_shell().filter(is_usable_codex_binary))
.or_else(|| find_usable_codex_binary(codex_binary_candidates()))
.ok_or_else(|| {
"Codex CLI not found. Install it: https://developers.openai.com/codex/cli".into()
})
}
fn find_codex_binary_on_path() -> Option<PathBuf> {
@@ -139,8 +133,12 @@ fn nvm_node_binary_candidates_for_home(home: &Path, binary_name: &str) -> Vec<Pa
candidates
}
fn find_existing_binary(candidates: Vec<PathBuf>) -> Result<Option<PathBuf>, String> {
crate::cli_agent_runtime::find_executable_binary_candidate(candidates, "Codex CLI")
fn find_usable_codex_binary(candidates: Vec<PathBuf>) -> Option<PathBuf> {
candidates.into_iter().find(is_usable_codex_binary)
}
fn is_usable_codex_binary(binary: &PathBuf) -> bool {
crate::cli_agent_runtime::version_for_binary(binary).is_some()
}
fn run_agent_stream_with_binary<F>(
@@ -186,9 +184,9 @@ fn build_codex_args(request: &AgentStreamRequest) -> Result<Vec<String>, String>
Ok(vec![
"--sandbox".into(),
"workspace-write".into(),
codex_sandbox(request.permission_mode).into(),
"--ask-for-approval".into(),
"never".into(),
codex_approval_policy(request.permission_mode).into(),
"exec".into(),
"--json".into(),
"-C".into(),
@@ -205,6 +203,20 @@ fn build_codex_args(request: &AgentStreamRequest) -> Result<Vec<String>, String>
])
}
fn codex_sandbox(permission_mode: crate::ai_agents::AiAgentPermissionMode) -> &'static str {
match permission_mode {
crate::ai_agents::AiAgentPermissionMode::Safe => "read-only",
crate::ai_agents::AiAgentPermissionMode::PowerUser => "workspace-write",
}
}
fn codex_approval_policy(permission_mode: crate::ai_agents::AiAgentPermissionMode) -> &'static str {
match permission_mode {
crate::ai_agents::AiAgentPermissionMode::Safe => "untrusted",
crate::ai_agents::AiAgentPermissionMode::PowerUser => "never",
}
}
fn build_codex_prompt(request: &AgentStreamRequest) -> String {
crate::cli_agent_runtime::build_prompt(&request.message, request.system_prompt.as_deref())
}
@@ -276,7 +288,7 @@ fn format_codex_error(stderr_output: String, status: String) -> String {
}
if is_codex_write_permission_error(&lower) {
return "Codex could not write to the active vault. Tolaria starts Codex with a workspace-write sandbox, so verify the selected vault folder is writable and retry; writes outside the active vault remain blocked.".into();
return "Codex could not write to the active vault. Vault Safe uses a read-only Codex sandbox; switch to Power User for shell-backed local writes, or verify the selected vault folder is writable and retry. Writes outside the active vault remain blocked.".into();
}
if stderr_output.trim().is_empty() {
@@ -331,13 +343,10 @@ mod tests {
}
}
fn assert_codex_workspace_write_contract(args: &[String]) {
let prefix = [
"--sandbox",
"workspace-write",
"--ask-for-approval",
"never",
];
fn assert_codex_permission_contract(args: &[String], permission_mode: AiAgentPermissionMode) {
let sandbox = codex_sandbox(permission_mode);
let approval = codex_approval_policy(permission_mode);
let prefix = ["--sandbox", sandbox, "--ask-for-approval", approval];
assert_eq!(&args[..prefix.len()], prefix);
assert!(!args.iter().any(|arg| arg == "danger-full-access"));
@@ -396,26 +405,21 @@ mod tests {
permission_mode: AiAgentPermissionMode::Safe,
}) {
assert_eq!(args[4], "exec");
assert_codex_workspace_write_contract(&args);
assert_codex_permission_contract(&args, AiAgentPermissionMode::Safe);
assert!(args.contains(&"--json".to_string()));
assert!(args.contains(&"-C".to_string()));
}
}
#[test]
fn codex_permission_modes_keep_workspace_write_without_dangerous_bypass() {
for permission_mode in [
AiAgentPermissionMode::Safe,
AiAgentPermissionMode::PowerUser,
] {
if let Ok(args) = build_codex_args(&AgentStreamRequest {
message: "Rename the note".into(),
system_prompt: None,
vault_path: "/tmp/vault".into(),
permission_mode,
}) {
assert_codex_workspace_write_contract(&args);
}
fn codex_power_user_keeps_workspace_write_without_dangerous_bypass() {
if let Ok(args) = build_codex_args(&AgentStreamRequest {
message: "Rename the note".into(),
system_prompt: None,
vault_path: "/tmp/vault".into(),
permission_mode: AiAgentPermissionMode::PowerUser,
}) {
assert_codex_permission_contract(&args, AiAgentPermissionMode::PowerUser);
}
}
@@ -522,6 +526,18 @@ exit 2
assert!(candidates.contains(&codex), "missing {}", codex.display());
}
#[cfg(unix)]
#[test]
fn usable_codex_binary_skips_broken_shims() {
let dir = tempfile::tempdir().unwrap();
let broken = executable_script(dir.path(), "broken-codex", "exit 1\n");
let working = executable_script(dir.path(), "codex", "echo codex-cli 0.124.0-alpha.2\n");
let found = find_usable_codex_binary(vec![broken, working.clone()]);
assert_eq!(found, Some(working));
}
#[test]
fn first_existing_path_skips_empty_and_missing_lines() {
let dir = tempfile::tempdir().unwrap();