feat(permissions): LP4-4 — enforcement Landlock sur le chemin structuré

Étend l'enforcement OS au chemin structuré (sessions Claude/Codex mode JSON),
jusqu'ici seulement advisory. Approche validée par l'Architecte : transposer la
technique du PTY plutôt qu'un pre_exec (rejeté — landlock alloue, deadlock malloc
post-fork en process multithreadé).

Mécanique (cfg(target_os=linux)) : run_turn_sandboxed/drain_sandboxed exécutent
enforce(plan) sur un thread jetable AVANT le spawn std, puis std::process::spawn
depuis ce thread ; l'enfant hérite le domaine Landlock via les credentials de la
tâche (garanti à travers fork/clone/execve, y compris posix_spawn — pas de pre_exec
nécessaire, forbid(unsafe_code) préservé). Fail-closed sur Err d'enforce (aucun
child). Timeout sous sandbox : oneshot killer + tokio::time::timeout → kill → EOF →
reap (pas de zombie/thread bloqué). Chemin non-sandboxé (plan None / pas d'enforcer /
non-Linux) = drain async tokio inchangé.

Contrat : SpawnLine.sandbox ; AgentSessionFactory::start(.., sandbox) ;
StructuredSessionFactory::with_sandbox_enforcer (jumeau du PTY) ; plan calculé en
step 5d de lifecycle relayé à launch_structured ; default_enforcer() injecté au
composition root.

Tests : 7 invariants e2e (parité, companion négatif, fail-closed, no-op natif,
confinement de l'irréversibilité entre tours, timeout, resume préservé) — zéro
token (sh/FakeCli). 80 suites vertes, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 08:47:33 +02:00
parent 7e01ac60cb
commit 6236cd727b
13 changed files with 821 additions and 39 deletions

View File

@ -28,7 +28,7 @@ use domain::{
ProjectedFile, ProjectionContext, ProjectorKey, Project, ProjectPath, ProviderSessionStore,
PtySize, SessionId, SessionKind, SessionStatus, Skill, TerminalSession,
};
use domain::sandbox::{compile_sandbox_plan, SandboxContext};
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
use crate::error::AppError;
use crate::layout::{persist_doc, resolve_doc};
@ -1569,6 +1569,7 @@ impl LaunchAgent {
&input.project.root,
input.node_id,
size,
spec.sandbox.as_ref(),
)
.await;
}
@ -1630,9 +1631,12 @@ impl LaunchAgent {
root: &ProjectPath,
node_id: Option<NodeId>,
size: PtySize,
sandbox: Option<&SandboxPlan>,
) -> Result<LaunchAgentOutput, AppError> {
// Relaie le plan de sandbox OS (lot LP4-4) à la fabrique : `spec.sandbox`,
// déjà compilé (pur, domaine) en step 5d. `None` ⇒ exécution native inchangée.
let session = factory
.start(profile, prepared, run_dir, session_plan)
.start(profile, prepared, run_dir, session_plan, sandbox)
.await
.map_err(|e| AppError::Process(e.to_string()))?;