Files
IdeA/crates/infrastructure/src/sandbox/mod.rs
Blomios 17ca65ed0f feat(permissions): LP4-1/LP4-2 — enforcer Landlock OS + consommation PTY du plan
LP4-1 : adaptateurs SandboxEnforcer concrets (LandlockSandbox Linux via le LSM
Landlock, NoopSandbox ailleurs) + default_enforcer() cfg-sélectionné, prêts à
être injectés au composition root (LP4-3). Aucun changement de comportement
runtime tant que rien n'est câblé : SpawnSpec.sandbox reste None.

LP4-2 : PortablePtyAdapter consomme SpawnSpec.sandbox via spawn_command_sandboxed
(thread jetable restreint par enforce() puis fork → le domaine Landlock est hérité
par l'enfant à travers execve ; les autres threads d'IdeA ne sont jamais sandboxés).
Builder additif with_sandbox_enforcer ; fail-closed si enforce() échoue.

Tests : domain + infrastructure verts (dont les tests Landlock réels de fencing
read-only/write-only et la confine-au-thread).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:00:47 +02:00

83 lines
2.9 KiB
Rust

//! OS-sandbox adapters (lot LP4-1) — concrete [`domain::sandbox::SandboxEnforcer`]
//! implementations and the cfg-selected default constructor.
//!
//! - [`LandlockSandbox`] (Linux only) enforces a [`domain::sandbox::SandboxPlan`]
//! via the kernel Landlock LSM.
//! - [`NoopSandbox`] (everywhere) enforces nothing — the non-Linux / fallback path.
//!
//! [`default_enforcer`] returns the right one for the build target as a
//! `Arc<dyn SandboxEnforcer>`, ready for the composition root (LP4-3) to inject.
//! Nothing here is wired into the launch path yet (that is LP4-2): until then the
//! enforcer stays unused and `SpawnSpec.sandbox` stays `None`, so there is zero
//! runtime behaviour change.
use std::sync::Arc;
use domain::sandbox::SandboxEnforcer;
mod noop;
pub use noop::NoopSandbox;
#[cfg(target_os = "linux")]
mod landlock;
#[cfg(target_os = "linux")]
pub use landlock::LandlockSandbox;
/// The default OS-sandbox enforcer for the current build target: [`LandlockSandbox`]
/// on Linux, [`NoopSandbox`] everywhere else.
#[cfg(target_os = "linux")]
#[must_use]
pub fn default_enforcer() -> Arc<dyn SandboxEnforcer> {
Arc::new(LandlockSandbox::new())
}
/// The default OS-sandbox enforcer for the current build target: [`NoopSandbox`]
/// on non-Linux platforms (no OS sandbox wired yet).
#[cfg(not(target_os = "linux"))]
#[must_use]
pub fn default_enforcer() -> Arc<dyn SandboxEnforcer> {
Arc::new(NoopSandbox::new())
}
#[cfg(test)]
mod tests {
use super::*;
use domain::sandbox::{SandboxKind, SandboxPlan, SandboxStatus};
use domain::Posture;
#[test]
fn noop_enforcer_is_unsupported_and_never_errors() {
use domain::sandbox::PathGrant;
let noop = NoopSandbox::new();
assert_eq!(noop.kind(), SandboxKind::Unsupported);
// Across every posture — and even with grants present — the no-op enforces
// nothing and never fails closed: there is no OS sandbox here to fail closed
// against (the Deny posture's teeth live only in the advisory LP3 projection).
for posture in [Posture::Allow, Posture::Ask, Posture::Deny] {
let plan = SandboxPlan {
allowed: vec![PathGrant {
abs_root: "/whatever".to_owned(),
access: domain::sandbox::PathAccess::RW,
}],
default_posture: posture,
};
assert_eq!(
noop.enforce(&plan).unwrap(),
SandboxStatus::Unsupported,
"no-op must return Unsupported (never Err) under posture {posture:?}"
);
}
}
#[test]
fn default_enforcer_matches_the_build_target() {
let enforcer = default_enforcer();
#[cfg(target_os = "linux")]
assert_eq!(enforcer.kind(), SandboxKind::Landlock);
#[cfg(not(target_os = "linux"))]
assert_eq!(enforcer.kind(), SandboxKind::Unsupported);
}
}