11//! Session discovery and management for the audit system.
22//!
3- //! Audit sessions are stored in `~/.nono/audit/`. For backwards
4- //! compatibility, the audit commands also read legacy audit metadata from
5- //! `~/.nono/rollbacks/` when no migrated audit entry exists yet.
3+ //! Audit sessions are stored under `$XDG_STATE_HOME/nono/audit/` (default
4+ //! `~/.local/state/nono/audit/`). For backwards compatibility, reads also
5+ //! check `~/.nono/audit/` until v1.0.0, and legacy audit metadata under
6+ //! `~/.nono/rollbacks/` (or the canonical rollback root) when no migrated
7+ //! audit entry exists yet.
68
7- use crate :: rollback_session ;
9+ use crate :: state_paths ;
810use nono:: undo:: { SessionMetadata , SnapshotManager } ;
911use nono:: { NonoError , Result } ;
1012use std:: collections:: BTreeSet ;
@@ -27,10 +29,9 @@ pub struct SessionInfo {
2729 pub is_stale : bool ,
2830}
2931
30- /// Get the audit root directory (`~/. nono/audit/`)
32+ /// Get the canonical audit root directory (`$XDG_STATE_HOME/ nono/audit/`).
3133pub fn audit_root ( ) -> Result < PathBuf > {
32- let home = dirs:: home_dir ( ) . ok_or ( NonoError :: HomeNotFound ) ?;
33- Ok ( home. join ( ".nono" ) . join ( "audit" ) )
34+ state_paths:: audit_root ( )
3435}
3536
3637/// Discover all audit sessions.
@@ -42,14 +43,12 @@ pub fn discover_sessions() -> Result<Vec<SessionInfo>> {
4243 let mut sessions = Vec :: new ( ) ;
4344 let mut seen_ids = BTreeSet :: new ( ) ;
4445 let primary_root = audit_root ( ) ?;
46+ let legacy_roots = state_paths:: LegacyRootSet :: resolve ( ) ?;
4547
46- for root in [
47- Some ( primary_root. clone ( ) ) ,
48- rollback_session:: rollback_root ( ) . ok ( ) ,
49- ] {
50- let Some ( root) = root else {
51- continue ;
52- } ;
48+ let mut roots: Vec < PathBuf > = state_paths:: audit_discovery_roots ( ) ?;
49+ roots. extend ( state_paths:: rollback_discovery_roots ( ) ?) ;
50+
51+ for root in roots {
5352 if !root. exists ( ) {
5453 continue ;
5554 }
@@ -86,6 +85,7 @@ pub fn discover_sessions() -> Result<Vec<SessionInfo>> {
8685 continue ;
8786 }
8887
88+ legacy_roots. warn_if_legacy_audit_data_read ( & dir) ;
8989 sessions. push ( build_session_info ( dir, metadata) ) ;
9090 }
9191 }
@@ -98,12 +98,11 @@ pub fn discover_sessions() -> Result<Vec<SessionInfo>> {
9898pub fn load_session ( session_id : & str ) -> Result < SessionInfo > {
9999 validate_session_id ( session_id) ?;
100100 let primary_root = audit_root ( ) ?;
101- let roots = [
102- Some ( primary_root. clone ( ) ) ,
103- rollback_session:: rollback_root ( ) . ok ( ) ,
104- ] ;
101+ let legacy_roots = state_paths:: LegacyRootSet :: resolve ( ) ?;
102+ let mut roots: Vec < PathBuf > = state_paths:: audit_discovery_roots ( ) ?;
103+ roots. extend ( state_paths:: rollback_discovery_roots ( ) ?) ;
105104
106- for root in roots. into_iter ( ) . flatten ( ) {
105+ for root in roots {
107106 let dir = root. join ( session_id) ;
108107 if !dir. exists ( ) {
109108 continue ;
@@ -129,6 +128,7 @@ pub fn load_session(session_id: &str) -> Result<SessionInfo> {
129128 continue ;
130129 }
131130
131+ legacy_roots. warn_if_legacy_audit_data_read ( & dir) ;
132132 return Ok ( build_session_info ( dir, metadata) ) ;
133133 }
134134
@@ -242,8 +242,11 @@ mod tests {
242242 fn discover_sessions_excludes_rollback_backed_entries ( ) {
243243 let _env_lock = ENV_LOCK . lock ( ) . unwrap ( ) ;
244244 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
245+ let state = tmp. path ( ) . join ( "state" ) ;
246+ fs:: create_dir_all ( & state) . unwrap ( ) ;
245247 let home = tmp. path ( ) . to_string_lossy ( ) . to_string ( ) ;
246- let _env = EnvVarGuard :: set_all ( & [ ( "HOME" , & home) ] ) ;
248+ let state_str = state. to_string_lossy ( ) . to_string ( ) ;
249+ let _env = EnvVarGuard :: set_all ( & [ ( "HOME" , & home) , ( "XDG_STATE_HOME" , & state_str) ] ) ;
247250
248251 let audit_dir = audit_root ( ) . unwrap ( ) . join ( "20260421-111111-10001" ) ;
249252 fs:: create_dir_all ( & audit_dir) . unwrap ( ) ;
@@ -267,7 +270,7 @@ mod tests {
267270 )
268271 . unwrap ( ) ;
269272
270- let legacy_audit_dir = rollback_session :: rollback_root ( )
273+ let legacy_audit_dir = state_paths :: legacy_rollback_root ( )
271274 . unwrap ( )
272275 . join ( "20260421-111111-10002" ) ;
273276 fs:: create_dir_all ( & legacy_audit_dir) . unwrap ( ) ;
@@ -291,7 +294,7 @@ mod tests {
291294 )
292295 . unwrap ( ) ;
293296
294- let rollback_dir = rollback_session :: rollback_root ( )
297+ let rollback_dir = state_paths :: legacy_rollback_root ( )
295298 . unwrap ( )
296299 . join ( "20260421-111111-10003" ) ;
297300 fs:: create_dir_all ( & rollback_dir) . unwrap ( ) ;
@@ -325,4 +328,95 @@ mod tests {
325328 assert ! ( ids. contains( & "20260421-111111-10002" ) ) ;
326329 assert ! ( !ids. contains( & "20260421-111111-10003" ) ) ;
327330 }
331+
332+ #[ test]
333+ fn discover_sessions_reads_legacy_audit_root ( ) {
334+ let _env_lock = ENV_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
335+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
336+ let state = tmp. path ( ) . join ( "state" ) ;
337+ fs:: create_dir_all ( & state) . unwrap ( ) ;
338+ let home = tmp. path ( ) . to_string_lossy ( ) . to_string ( ) ;
339+ let state_str = state. to_string_lossy ( ) . to_string ( ) ;
340+ let _env = EnvVarGuard :: set_all ( & [ ( "HOME" , & home) , ( "XDG_STATE_HOME" , & state_str) ] ) ;
341+
342+ let legacy_audit_dir = state_paths:: legacy_audit_root ( )
343+ . unwrap ( )
344+ . join ( "20260421-111111-20001" ) ;
345+ fs:: create_dir_all ( & legacy_audit_dir) . unwrap ( ) ;
346+ SnapshotManager :: write_session_metadata (
347+ & legacy_audit_dir,
348+ & SessionMetadata {
349+ session_id : "20260421-111111-20001" . to_string ( ) ,
350+ started : "2026-04-21T11:11:11+01:00" . to_string ( ) ,
351+ ended : Some ( "2026-04-21T11:11:12+01:00" . to_string ( ) ) ,
352+ command : vec ! [ "/bin/echo" . to_string( ) ] ,
353+ executable_identity : None ,
354+ tracked_paths : vec ! [ PathBuf :: from( "/tmp/work" ) ] ,
355+ snapshot_count : 0 ,
356+ exit_code : Some ( 0 ) ,
357+ merkle_roots : Vec :: new ( ) ,
358+ network_events : Vec :: new ( ) ,
359+ audit_event_count : 1 ,
360+ audit_integrity : None ,
361+ audit_attestation : None ,
362+ } ,
363+ )
364+ . unwrap ( ) ;
365+
366+ let sessions = discover_sessions ( ) . unwrap ( ) ;
367+ let ids: Vec < _ > = sessions
368+ . iter ( )
369+ . map ( |s| s. metadata . session_id . as_str ( ) )
370+ . collect ( ) ;
371+ assert ! ( ids. contains( & "20260421-111111-20001" ) ) ;
372+ assert ! ( is_legacy_audit_only_session(
373+ sessions
374+ . iter( )
375+ . find( |s| s. metadata. session_id == "20260421-111111-20001" )
376+ . expect( "legacy session" )
377+ ) ) ;
378+ }
379+
380+ #[ test]
381+ fn discover_sessions_does_not_warn_when_legacy_audit_root_is_empty ( ) {
382+ let _env_lock = ENV_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
383+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
384+ let state = tmp. path ( ) . join ( "state" ) ;
385+ fs:: create_dir_all ( & state) . unwrap ( ) ;
386+ let home = tmp. path ( ) . to_string_lossy ( ) . to_string ( ) ;
387+ let state_str = state. to_string_lossy ( ) . to_string ( ) ;
388+ let _env = EnvVarGuard :: set_all ( & [ ( "HOME" , & home) , ( "XDG_STATE_HOME" , & state_str) ] ) ;
389+
390+ let legacy_root = state_paths:: legacy_audit_root ( ) . unwrap ( ) ;
391+ fs:: create_dir_all ( & legacy_root) . unwrap ( ) ;
392+ fs:: write ( legacy_root. join ( "ledger.ndjson" ) , b"{}\n " ) . unwrap ( ) ;
393+
394+ let canonical_dir = state_paths:: audit_root ( )
395+ . unwrap ( )
396+ . join ( "20260421-111111-30001" ) ;
397+ fs:: create_dir_all ( & canonical_dir) . unwrap ( ) ;
398+ SnapshotManager :: write_session_metadata (
399+ & canonical_dir,
400+ & SessionMetadata {
401+ session_id : "20260421-111111-30001" . to_string ( ) ,
402+ started : "2026-04-21T11:11:11+01:00" . to_string ( ) ,
403+ ended : Some ( "2026-04-21T11:11:12+01:00" . to_string ( ) ) ,
404+ command : vec ! [ "/bin/echo" . to_string( ) ] ,
405+ executable_identity : None ,
406+ tracked_paths : vec ! [ PathBuf :: from( "/tmp/work" ) ] ,
407+ snapshot_count : 0 ,
408+ exit_code : Some ( 0 ) ,
409+ merkle_roots : Vec :: new ( ) ,
410+ network_events : Vec :: new ( ) ,
411+ audit_event_count : 1 ,
412+ audit_integrity : None ,
413+ audit_attestation : None ,
414+ } ,
415+ )
416+ . unwrap ( ) ;
417+
418+ let sessions = discover_sessions ( ) . unwrap ( ) ;
419+ assert_eq ! ( sessions. len( ) , 1 ) ;
420+ assert ! ( is_primary_audit_session( & sessions[ 0 ] . dir) ) ;
421+ }
328422}
0 commit comments