feat: phase 2 — audit config + update --rollback (closes #63 #64)

Axe 8 (sécurité) :
- am audit : checksums SHA-256 des configs (embarquée, utilisateur,
  locale) stockés dans state.json, croisement avec events.jsonl pour
  dater les changements, détection des modifications manuelles hors de
  am (statut MANUAL), rapport lisible + --json stable (contrat)
- backup automatique avant chaque am update (agent ou --all) : état +
  config utilisateur + répertoires d'installation + shims dans
  <state>/backups/<ts>/ avec manifest.json
- am update --rollback [point] : restaure le dernier backup (ou un id
  précis, préfixe accepté), arrête les agents concernés d'abord,
  événement EventKind::Rollback journalisé
- am update --rollback list : liste les points de restauration
- rétention settings.backups_keep (défaut 5) appliquée après chaque backup
- événements EventKind::Backup / EventKind::Rollback pour l'audit

REPL : commande audit + update --rollback (parse, complétion), help specs,
tip cheat sheet, man pages régénérées (50 pages). Version 0.4.6,
ROADMAP cases cochées, 272 tests verts.
This commit is contained in:
2026-08-18 09:44:16 -04:00
parent 22bc529818
commit d9ffc34156
21 changed files with 971 additions and 33 deletions
+5 -3
View File
@@ -1,6 +1,7 @@
use agent_manager::commands::update_cmd;
use agent_manager::config::UpdatePolicy;
use agent_manager::state::{InstalledEntry, StateFile};
use clap::Parser;
use std::collections::BTreeMap;
mod common;
@@ -26,6 +27,7 @@ fn installed_state(name: &str, version: &str) -> StateFile {
sessions_count: 0,
last_used: None,
annotations: BTreeMap::new(),
config_checksums: BTreeMap::new(),
}
}
@@ -44,7 +46,7 @@ fn update_policy_none_blocks_all() {
let mut app = common::test_app("policy_none", &["--yes"]);
app.config.settings.update_policy = Some(UpdatePolicy::None);
app.state.save(&installed_state("pi", "0.1.0")).unwrap();
let code = update_cmd::run(&app, None, true).unwrap();
let code = update_cmd::run(&app, None, true, None).unwrap();
assert_eq!(code, 0);
}
@@ -53,7 +55,7 @@ fn pinned_agent_is_skipped() {
let body = agent_yaml("pi", Some("0.1.0"));
let app = common::test_app("pinned_ok", &[]);
app.state.save(&installed_state("pi", "0.1.0")).unwrap();
let code = update_cmd::run(&app, Some("pi"), false).unwrap();
let code = update_cmd::run(&app, Some("pi"), false, None).unwrap();
assert_eq!(code, 0);
}
@@ -62,6 +64,6 @@ fn pinned_agent_mismatch_warns() {
let body = agent_yaml("pi", Some("0.1.0"));
let app = common::test_app("pinned_warn", &[]);
app.state.save(&installed_state("pi", "0.2.0")).unwrap();
let code = update_cmd::run(&app, Some("pi"), false).unwrap();
let code = update_cmd::run(&app, Some("pi"), false, None).unwrap();
assert_eq!(code, 0);
}