Composition root : PortablePtyAdapter::new() injecte désormais default_enforcer()
(Landlock sur Linux / Noop ailleurs) via with_sandbox_enforcer, dans state.rs et
dans LocalHost (remote/mod.rs).
Launch path : LaunchAgent::execute peuple spec.sandbox via le plan pur
compile_sandbox_plan(effective_permissions, SandboxContext{project_root, run_dir}),
en réutilisant l'EffectivePermissions déjà résolu pour la projection advisory LP3.
Invariant produit conservé : eff == None ⇒ plan None ⇒ comportement natif, aucune
projection OS.
Portée : seul le chemin PTY brut est OS-enforcé ; le chemin structuré (Claude/Codex
mode JSON) porte le plan mais ne l'enforce pas encore (lot ultérieur).
Tests : ajout d'un test d'intégration bout-en-bout (pty/mod.rs, sandbox_e2e_tests)
prouvant qu'un enfant spawné sous un plan RW restreint ne peut PAS écrire hors-grant
(écriture bloquée par le kernel) alors que l'écriture dans le grant passe, plus un
test compagnon anti faux-positif (sans plan ⇒ pas de sandbox). Skip propre si le
kernel n'a pas Landlock. domain+infrastructure+application+app-tauri verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.2 KiB
Rust
98 lines
3.2 KiB
Rust
//! Remote-host strategy adapters (ARCHITECTURE §5, L9).
|
|
//!
|
|
//! A [`RemoteHost`] is the **factory** that decides *where* a project's I/O
|
|
//! happens — it hands out the location-appropriate [`FileSystem`],
|
|
//! [`ProcessSpawner`] and [`PtyPort`]. Routing every use case through the
|
|
//! project's host is what makes local and remote execution transparent (Liskov,
|
|
//! ARCHITECTURE §1.2).
|
|
//!
|
|
//! This module ships [`LocalHost`] (the local strategy, fully wired and tested)
|
|
//! and the [`remote_host`] selector. The SSH and WSL strategies (`SshHost` via
|
|
//! russh/SFTP, `WslHost` via `wsl.exe`) are the remaining L9 work; their
|
|
//! integration tests are environment-gated (no SSH server / no WSL here), so they
|
|
//! are not yet wired in to avoid shipping unverified adapters. Until then the
|
|
//! selector reports them as unsupported rather than silently failing later.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use domain::ports::{FileSystem, ProcessSpawner, PtyPort, RemoteError, RemoteHost};
|
|
use domain::remote::{RemoteKind, RemoteRef};
|
|
|
|
use crate::{LocalFileSystem, LocalProcessSpawner, PortablePtyAdapter};
|
|
|
|
/// The local execution strategy: the project lives on this machine, so the host
|
|
/// simply hands out the local adapters.
|
|
#[derive(Clone)]
|
|
pub struct LocalHost {
|
|
fs: Arc<dyn FileSystem>,
|
|
spawner: Arc<dyn ProcessSpawner>,
|
|
pty: Arc<dyn PtyPort>,
|
|
}
|
|
|
|
impl LocalHost {
|
|
/// Builds a local host wired to the local FS / process / PTY adapters.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
fs: Arc::new(LocalFileSystem::new()),
|
|
spawner: Arc::new(LocalProcessSpawner::new()),
|
|
pty: Arc::new(
|
|
PortablePtyAdapter::new().with_sandbox_enforcer(crate::sandbox::default_enforcer()),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for LocalHost {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl RemoteHost for LocalHost {
|
|
fn kind(&self) -> RemoteKind {
|
|
RemoteKind::Local
|
|
}
|
|
|
|
async fn connect(&self) -> Result<(), RemoteError> {
|
|
// Nothing to establish for the local machine.
|
|
Ok(())
|
|
}
|
|
|
|
fn file_system(&self) -> Arc<dyn FileSystem> {
|
|
Arc::clone(&self.fs)
|
|
}
|
|
|
|
fn process_spawner(&self) -> Arc<dyn ProcessSpawner> {
|
|
Arc::clone(&self.spawner)
|
|
}
|
|
|
|
fn pty(&self) -> Arc<dyn PtyPort> {
|
|
Arc::clone(&self.pty)
|
|
}
|
|
}
|
|
|
|
/// Selects and builds the [`RemoteHost`] for a [`RemoteRef`].
|
|
///
|
|
/// `Local` yields a [`LocalHost`]. `Ssh`/`Wsl` are not yet wired (their adapters
|
|
/// are the remaining, environment-gated L9 work) and return a clear
|
|
/// [`RemoteError::Connection`] so callers fail fast with an actionable message
|
|
/// instead of a confusing downstream error.
|
|
///
|
|
/// # Errors
|
|
/// [`RemoteError::Connection`] for SSH/WSL remotes until their adapters land.
|
|
pub fn remote_host(remote: &RemoteRef) -> Result<Arc<dyn RemoteHost>, RemoteError> {
|
|
match remote {
|
|
RemoteRef::Local => Ok(Arc::new(LocalHost::new())),
|
|
RemoteRef::Ssh { host, .. } => Err(RemoteError::Connection(format!(
|
|
"SSH remote ({host}) is not yet supported"
|
|
))),
|
|
RemoteRef::Wsl { distro } => Err(RemoteError::Connection(format!(
|
|
"WSL remote ({distro}) is not yet supported"
|
|
))),
|
|
}
|
|
}
|