Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
214 lines
8.2 KiB
Rust
214 lines
8.2 KiB
Rust
//! Codex CLI permission projector (lot LP3-2).
|
|
//!
|
|
//! Produces the **permission-relevant** part of Codex's `config.toml`
|
|
//! (`sandbox_mode` / `approval_policy`) plus the matching launch args
|
|
//! (`--sandbox` / `--ask-for-approval`) and, for workspace-write postures, the
|
|
//! project root as an additional writable directory (`--add-dir`). The
|
|
//! posture→mode derivation is extracted verbatim from the former
|
|
//! `codex_sandbox_mode` / `codex_approval_policy` / `apply_codex_cli_permission_args`
|
|
//! in `lifecycle.rs`.
|
|
//!
|
|
//! Unlike Claude's seed, Codex's `config.toml` is **co-owned** (it also carries the
|
|
//! `mcp_servers.idea` table and the `projects.*` trust entries, which are MCP/trust
|
|
//! concerns, not permissions). The projector therefore emits a
|
|
//! [`ProjectedFile::MergeToml`] limited to the two permission keys it manages —
|
|
//! everything else in the file is preserved by the fold, and the file is **never**
|
|
//! deleted on swap (hence an empty `owned_replace_paths`).
|
|
|
|
use domain::permission::{
|
|
EffectivePermissions, PermissionProjection, PermissionProjector, Posture, ProjectedFile,
|
|
ProjectionContext, ProjectorKey,
|
|
};
|
|
|
|
use super::toml_string;
|
|
|
|
/// Run-dir-relative path of Codex's `config.toml`. Codex reads its config from
|
|
/// `$CODEX_HOME/config.toml`; IdeA isolates `CODEX_HOME` to `{runDir}/.codex`, so
|
|
/// the file lives at `.codex/config.toml` relative to the run dir.
|
|
const CONFIG_REL_PATH: &str = ".codex/config.toml";
|
|
|
|
/// The two top-level keys this projector manages in `config.toml`. Everything else
|
|
/// (MCP table, trust entries, user keys) is preserved by the merge.
|
|
const MANAGED_KEYS: [&str; 2] = ["sandbox_mode", "approval_policy"];
|
|
|
|
/// Projects [`EffectivePermissions`] into Codex's sandbox/approval config + args.
|
|
///
|
|
/// Pure: `project` only computes the plan; the launch path merges the TOML fragment
|
|
/// and appends the args.
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub struct CodexPermissionProjector;
|
|
|
|
impl PermissionProjector for CodexPermissionProjector {
|
|
fn key(&self) -> ProjectorKey {
|
|
ProjectorKey::Codex
|
|
}
|
|
|
|
fn project(
|
|
&self,
|
|
eff: Option<&EffectivePermissions>,
|
|
ctx: &ProjectionContext,
|
|
) -> PermissionProjection {
|
|
// Product invariant: nothing posed ⇒ nothing projected. Codex keeps its
|
|
// native sandbox/approval defaults (no args, no managed keys written).
|
|
let Some(permissions) = eff else {
|
|
return PermissionProjection::empty();
|
|
};
|
|
let sandbox = codex_sandbox_mode(permissions);
|
|
let approval = codex_approval_policy(permissions);
|
|
|
|
// Permission-only TOML fragment (escaped exactly like the former
|
|
// `set_top_level_toml_value`). The mcp_servers/trust tables are NOT a
|
|
// permission concern and stay with the MCP wiring (LP3-3).
|
|
let contents = format!(
|
|
"sandbox_mode = {}\napproval_policy = {}\n",
|
|
toml_string(sandbox),
|
|
toml_string(approval),
|
|
);
|
|
|
|
let mut args = vec![
|
|
"--sandbox".to_owned(),
|
|
sandbox.to_owned(),
|
|
"--ask-for-approval".to_owned(),
|
|
approval.to_owned(),
|
|
];
|
|
if sandbox == "workspace-write" && !ctx.project_root.is_empty() {
|
|
args.push("--add-dir".to_owned());
|
|
args.push(ctx.project_root.to_owned());
|
|
}
|
|
|
|
PermissionProjection {
|
|
files: vec![ProjectedFile::MergeToml {
|
|
rel_path: CONFIG_REL_PATH.to_owned(),
|
|
managed_tables: Vec::new(),
|
|
managed_keys: MANAGED_KEYS.iter().map(|k| (*k).to_owned()).collect(),
|
|
contents,
|
|
}],
|
|
args,
|
|
env: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn owned_replace_paths(&self) -> Vec<String> {
|
|
// config.toml is co-owned (MergeToml), never an owned Replace file.
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
fn codex_sandbox_mode(permissions: &EffectivePermissions) -> &'static str {
|
|
match permissions.fallback() {
|
|
Posture::Deny => "read-only",
|
|
Posture::Ask | Posture::Allow => "workspace-write",
|
|
}
|
|
}
|
|
|
|
fn codex_approval_policy(permissions: &EffectivePermissions) -> &'static str {
|
|
match permissions.fallback() {
|
|
Posture::Allow => "never",
|
|
Posture::Ask | Posture::Deny => "on-request",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use domain::permission::{resolve, PermissionSet};
|
|
|
|
fn ctx<'a>() -> ProjectionContext<'a> {
|
|
ProjectionContext {
|
|
project_root: "/proj",
|
|
run_dir: "/run/agent",
|
|
}
|
|
}
|
|
|
|
/// Builds an [`EffectivePermissions`] with the given fallback posture via the
|
|
/// domain API (only the fallback drives Codex's sandbox/approval derivation).
|
|
fn eff(fallback: Posture) -> EffectivePermissions {
|
|
resolve(Some(&PermissionSet::new(vec![], fallback)), None).unwrap()
|
|
}
|
|
|
|
// ---- product invariant + ownership ----------------------------------
|
|
|
|
#[test]
|
|
fn project_none_is_empty() {
|
|
let proj = CodexPermissionProjector.project(None, &ctx());
|
|
assert!(proj.files.is_empty());
|
|
assert!(proj.args.is_empty());
|
|
assert!(proj.env.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn owned_replace_paths_is_empty() {
|
|
// config.toml is co-owned (MergeToml) ⇒ nothing to clean up on swap.
|
|
assert!(CodexPermissionProjector.owned_replace_paths().is_empty());
|
|
}
|
|
|
|
// ---- (6) posture → sandbox_mode / approval_policy + (7) args↔contents
|
|
// coherence + add-dir + MergeToml shape ---------------------
|
|
|
|
#[test]
|
|
fn posture_maps_sandbox_and_approval_in_file_and_args() {
|
|
for (posture, sandbox, approval) in [
|
|
(Posture::Deny, "read-only", "on-request"),
|
|
(Posture::Ask, "workspace-write", "on-request"),
|
|
(Posture::Allow, "workspace-write", "never"),
|
|
] {
|
|
let proj = CodexPermissionProjector.project(Some(&eff(posture)), &ctx());
|
|
assert!(proj.env.is_empty(), "Codex projection carries no env");
|
|
|
|
// -- The single MergeToml file, with the two managed permission keys.
|
|
assert_eq!(proj.files.len(), 1, "exactly one file projected");
|
|
match &proj.files[0] {
|
|
ProjectedFile::MergeToml {
|
|
rel_path,
|
|
managed_tables,
|
|
managed_keys,
|
|
contents,
|
|
} => {
|
|
assert_eq!(rel_path, CONFIG_REL_PATH);
|
|
assert!(managed_tables.is_empty(), "no managed tables");
|
|
assert_eq!(
|
|
managed_keys,
|
|
&vec!["sandbox_mode".to_owned(), "approval_policy".to_owned()]
|
|
);
|
|
assert!(
|
|
contents.contains(&format!("sandbox_mode = \"{sandbox}\"")),
|
|
"posture {posture:?}: contents={contents:?}"
|
|
);
|
|
assert!(
|
|
contents.contains(&format!("approval_policy = \"{approval}\"")),
|
|
"posture {posture:?}: contents={contents:?}"
|
|
);
|
|
}
|
|
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
|
|
}
|
|
|
|
// -- (7) Args reflect the SAME values as the TOML, in CLI order.
|
|
assert_eq!(
|
|
&proj.args[..4],
|
|
vec![
|
|
"--sandbox".to_owned(),
|
|
sandbox.to_owned(),
|
|
"--ask-for-approval".to_owned(),
|
|
approval.to_owned(),
|
|
],
|
|
"posture {posture:?}: args must mirror the TOML values"
|
|
);
|
|
if sandbox == "workspace-write" {
|
|
assert!(
|
|
proj.args
|
|
.windows(2)
|
|
.any(|w| w == ["--add-dir".to_owned(), "/proj".to_owned()]),
|
|
"workspace-write posture must add the project root as writable: {:?}",
|
|
proj.args
|
|
);
|
|
} else {
|
|
assert!(
|
|
!proj.args.contains(&"--add-dir".to_owned()),
|
|
"read-only posture must not add writable dirs: {:?}",
|
|
proj.args
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|