feat: add SidecarManager actor pattern, SPKI pinning, btmsg seen_messages, Aider autonomous mode

Tribunal priorities 1-4: SidecarManager refactored to mpsc actor thread
(eliminates TOCTOU race), SPKI TOFU certificate pinning for relay TLS,
per-message btmsg acknowledgment via seen_messages table, Aider
autonomous mode toggle gating shell execution.
This commit is contained in:
Hibryda 2026-03-14 04:39:40 +01:00
parent 949d90887d
commit 23b4d0cf26
22 changed files with 1273 additions and 297 deletions

View file

@ -85,6 +85,39 @@ impl SandboxConfig {
}
}
/// Build a restricted sandbox config for Aider agent sessions.
/// More restrictive than `for_projects`: only project worktree + read-only system paths.
/// Does NOT allow write access to ~/.config, ~/.claude, etc.
pub fn for_aider_restricted(project_cwd: &str, worktree: Option<&str>) -> Self {
let mut rw = vec![PathBuf::from(project_cwd)];
if let Some(wt) = worktree {
rw.push(PathBuf::from(wt));
}
rw.push(std::env::temp_dir());
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/root"));
rw.push(home.join(".aider"));
let ro = vec![
PathBuf::from("/usr"),
PathBuf::from("/lib"),
PathBuf::from("/lib64"),
PathBuf::from("/etc"),
PathBuf::from("/proc"),
PathBuf::from("/dev"),
PathBuf::from("/bin"),
PathBuf::from("/sbin"),
home.join(".local"),
home.join(".deno"),
home.join(".nvm"),
];
Self {
rw_paths: rw,
ro_paths: ro,
enabled: true,
}
}
/// Build a sandbox config for a single project directory.
pub fn for_project(cwd: &str, worktree: Option<&str>) -> Self {
let worktrees: Vec<&str> = worktree.into_iter().collect();
@ -266,6 +299,57 @@ mod tests {
assert_eq!(config.rw_paths.len(), 3);
}
#[test]
fn test_for_aider_restricted_single_cwd() {
let config = SandboxConfig::for_aider_restricted("/home/user/myproject", None);
assert!(config.enabled);
assert!(config.rw_paths.contains(&PathBuf::from("/home/user/myproject")));
assert!(config.rw_paths.contains(&std::env::temp_dir()));
let home = dirs::home_dir().unwrap();
assert!(config.rw_paths.contains(&home.join(".aider")));
// No worktree path added
assert!(!config
.rw_paths
.iter()
.any(|p| p.to_string_lossy().contains("worktree")));
}
#[test]
fn test_for_aider_restricted_with_worktree() {
let config = SandboxConfig::for_aider_restricted(
"/home/user/myproject",
Some("/home/user/myproject/.claude/worktrees/abc123"),
);
assert!(config.enabled);
assert!(config.rw_paths.contains(&PathBuf::from("/home/user/myproject")));
assert!(config.rw_paths.contains(&PathBuf::from(
"/home/user/myproject/.claude/worktrees/abc123"
)));
}
#[test]
fn test_for_aider_restricted_no_config_write() {
let config = SandboxConfig::for_aider_restricted("/tmp/test", None);
let home = dirs::home_dir().unwrap();
// Aider restricted must NOT have ~/.config or ~/.claude in rw_paths
assert!(!config.rw_paths.contains(&home.join(".config")));
assert!(!config.rw_paths.contains(&home.join(".claude")));
// And NOT in ro_paths either (stricter than for_projects)
assert!(!config.ro_paths.contains(&home.join(".config")));
assert!(!config.ro_paths.contains(&home.join(".claude")));
}
#[test]
fn test_for_aider_restricted_rw_count() {
// Without worktree: cwd + tmp + .aider = 3
let config = SandboxConfig::for_aider_restricted("/tmp/test", None);
assert_eq!(config.rw_paths.len(), 3);
// With worktree: cwd + worktree + tmp + .aider = 4
let config = SandboxConfig::for_aider_restricted("/tmp/test", Some("/tmp/wt"));
assert_eq!(config.rw_paths.len(), 4);
}
#[test]
fn test_for_projects_empty() {
let config = SandboxConfig::for_projects(&[], &[]);

File diff suppressed because it is too large Load diff