fix(sandbox): writable roots Codex dérivées des permissions effectives du menu agent — #197 (QA verte)

This commit is contained in:
2026-08-14 15:05:15 +02:00
parent 6a98752cb0
commit a60fb3ed92
6 changed files with 482 additions and 146 deletions

View File

@ -0,0 +1,101 @@
use domain::permission::{Capability, Effect, EffectivePermissions, Posture};
use domain::ports::StructuredProviderLaunchPolicy;
use domain::sandbox::{compile_sandbox_plan, PathAccess, SandboxContext};
use domain::NetworkPolicy;
pub(crate) fn compile_codex_launch_policy(
project_root: &str,
run_dir: &str,
permissions: Option<&EffectivePermissions>,
network: Option<NetworkPolicy>,
extra_writable_roots: &[String],
) -> StructuredProviderLaunchPolicy {
let mut writable_roots = codex_permission_writable_roots(project_root, run_dir, permissions);
append_unique_paths(&mut writable_roots, extra_writable_roots);
let sandbox_mode = codex_sandbox_mode(permissions, &writable_roots).to_owned();
if sandbox_mode != "workspace-write" {
writable_roots.clear();
}
StructuredProviderLaunchPolicy::Codex {
sandbox_mode,
writable_roots,
network_access: codex_network_access(network),
}
}
pub(crate) fn codex_permission_writable_roots(
project_root: &str,
run_dir: &str,
permissions: Option<&EffectivePermissions>,
) -> Vec<String> {
let Some(permissions) = permissions else {
return Vec::new();
};
let mut roots = Vec::new();
if !has_rw_deny(permissions)
&& permissions.fallback() != Posture::Deny
&& permissions.decide_file(Capability::Write, "") != Posture::Deny
{
roots.push(project_root.to_owned());
}
if let Some(plan) = compile_sandbox_plan(
Some(permissions),
&SandboxContext {
project_root,
run_dir,
},
) {
for grant in plan
.allowed
.iter()
.filter(|grant| grant.access.contains(PathAccess::RW))
{
append_unique_path(&mut roots, &grant.abs_root);
}
}
roots.sort();
roots
}
pub(crate) fn codex_sandbox_mode(
permissions: Option<&EffectivePermissions>,
writable_roots: &[String],
) -> &'static str {
if !writable_roots.is_empty() {
return "workspace-write";
}
if permissions.is_some_and(has_rw_deny) {
return "read-only";
}
match permissions.map(EffectivePermissions::fallback) {
Some(Posture::Ask | Posture::Allow) | None => "workspace-write",
Some(Posture::Deny) => "read-only",
}
}
fn has_rw_deny(permissions: &EffectivePermissions) -> bool {
permissions.rules().iter().any(|rule| {
rule.effect() == Effect::Deny
&& matches!(rule.capability(), Capability::Write | Capability::Delete)
})
}
fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
matches!(network, Some(NetworkPolicy::Allow))
}
fn append_unique_paths(target: &mut Vec<String>, roots: &[String]) {
for root in roots {
append_unique_path(target, root);
}
}
fn append_unique_path(target: &mut Vec<String>, root: &str) {
if root.is_empty() || target.iter().any(|existing| existing == root) {
return;
}
target.push(root.to_owned());
}