@@ -24,45 +24,22 @@ pub(super) async fn run(command: SessionCommand) -> anyhow::Result<()> {
2424 . load_session_policy_snapshot ( & session_id)
2525 . with_context ( || format ! ( "load session policy snapshot: {session_id}" ) ) ?;
2626
27- let ( mcp_servers, mcp_config_sanitized, mcp_parse_error) =
28- summarize_mcp_config ( snapshot. mcp_config_json . as_deref ( ) , show_mcp_config) ;
29-
3027 if json {
31- let mut out = BTreeMap :: new ( ) ;
32- out. insert ( "session_id" . to_string ( ) , Value :: String ( session_id) ) ;
33- out. insert (
34- "allowed_tools" . to_string ( ) ,
35- serde_json:: to_value ( & snapshot. allowed_tools ) ?,
36- ) ;
37- out. insert (
38- "allowed_skills" . to_string ( ) ,
39- serde_json:: to_value ( & snapshot. allowed_skills ) ?,
40- ) ;
41- out. insert (
42- "skill_policy" . to_string ( ) ,
43- serde_json:: to_value ( & snapshot. skill_policy ) ?,
44- ) ;
45-
46- let mut mcp = BTreeMap :: new ( ) ;
47- mcp. insert (
48- "present" . to_string ( ) ,
49- Value :: Bool ( snapshot. mcp_config_json . is_some ( ) ) ,
50- ) ;
51- if let Some ( servers) = mcp_servers {
52- mcp. insert ( "servers" . to_string ( ) , serde_json:: to_value ( servers) ?) ;
53- }
54- if let Some ( err) = mcp_parse_error {
55- mcp. insert ( "parse_error" . to_string ( ) , Value :: String ( err) ) ;
56- }
57- if let Some ( cfg) = mcp_config_sanitized {
58- mcp. insert ( "config" . to_string ( ) , cfg) ;
59- }
60- out. insert ( "mcp" . to_string ( ) , Value :: Object ( mcp. into_iter ( ) . collect ( ) ) ) ;
61-
28+ let out = build_session_policy_json (
29+ session_id,
30+ & snapshot. allowed_tools ,
31+ & snapshot. allowed_skills ,
32+ & snapshot. skill_policy ,
33+ snapshot. mcp_config_json . as_deref ( ) ,
34+ show_mcp_config,
35+ ) ?;
6236 println ! ( "{}" , serde_json:: to_string_pretty( & out) ?) ;
6337 return Ok ( ( ) ) ;
6438 }
6539
40+ let ( mcp_servers, mcp_config_sanitized, mcp_parse_error) =
41+ summarize_mcp_config ( snapshot. mcp_config_json . as_deref ( ) , show_mcp_config) ;
42+
6643 println ! ( "Session policy" ) ;
6744 println ! ( ) ;
6845 println ! ( "session_id: {session_id}" ) ;
@@ -112,6 +89,51 @@ pub(super) async fn run(command: SessionCommand) -> anyhow::Result<()> {
11289 }
11390}
11491
92+ fn build_session_policy_json (
93+ session_id : String ,
94+ allowed_tools : & Option < Vec < String > > ,
95+ allowed_skills : & Option < Vec < String > > ,
96+ skill_policy : & rexos:: agent:: SessionSkillPolicy ,
97+ mcp_config_json : Option < & str > ,
98+ show_mcp_config : bool ,
99+ ) -> anyhow:: Result < Value > {
100+ let ( mcp_servers, mcp_config_sanitized, mcp_parse_error) =
101+ summarize_mcp_config ( mcp_config_json, show_mcp_config) ;
102+
103+ let mut out = BTreeMap :: new ( ) ;
104+ out. insert ( "session_id" . to_string ( ) , Value :: String ( session_id) ) ;
105+ out. insert (
106+ "allowed_tools" . to_string ( ) ,
107+ serde_json:: to_value ( allowed_tools) ?,
108+ ) ;
109+ out. insert (
110+ "allowed_skills" . to_string ( ) ,
111+ serde_json:: to_value ( allowed_skills) ?,
112+ ) ;
113+ out. insert (
114+ "skill_policy" . to_string ( ) ,
115+ serde_json:: to_value ( skill_policy) ?,
116+ ) ;
117+
118+ let mut mcp = BTreeMap :: new ( ) ;
119+ mcp. insert (
120+ "present" . to_string ( ) ,
121+ Value :: Bool ( mcp_config_json. is_some ( ) ) ,
122+ ) ;
123+ if let Some ( servers) = mcp_servers {
124+ mcp. insert ( "servers" . to_string ( ) , serde_json:: to_value ( servers) ?) ;
125+ }
126+ if let Some ( err) = mcp_parse_error {
127+ mcp. insert ( "parse_error" . to_string ( ) , Value :: String ( err) ) ;
128+ }
129+ if let Some ( cfg) = mcp_config_sanitized {
130+ mcp. insert ( "config" . to_string ( ) , cfg) ;
131+ }
132+ out. insert ( "mcp" . to_string ( ) , Value :: Object ( mcp. into_iter ( ) . collect ( ) ) ) ;
133+
134+ Ok ( Value :: Object ( out. into_iter ( ) . collect ( ) ) )
135+ }
136+
115137fn summarize_mcp_config (
116138 raw_json : Option < & str > ,
117139 include_sanitized : bool ,
@@ -130,7 +152,11 @@ fn summarize_mcp_config(
130152 let servers = parsed
131153 . get ( "servers" )
132154 . and_then ( |v| v. as_object ( ) )
133- . map ( |obj| obj. keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) ) ;
155+ . map ( |obj| {
156+ let mut servers = obj. keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) ;
157+ servers. sort ( ) ;
158+ servers
159+ } ) ;
134160
135161 let sanitized = if include_sanitized {
136162 Some ( sanitize_mcp_config ( & parsed) )
@@ -175,6 +201,109 @@ fn sanitize_mcp_config(value: &Value) -> Value {
175201mod tests {
176202 use super :: * ;
177203
204+ #[ test]
205+ fn build_session_policy_json_includes_expected_keys ( ) {
206+ let mut skill_policy = rexos:: agent:: SessionSkillPolicy :: default ( ) ;
207+ skill_policy. allowlist = vec ! [ "alpha" . to_string( ) ] ;
208+ skill_policy. require_approval = true ;
209+ skill_policy. auto_approve_readonly = false ;
210+
211+ let mcp_config_json =
212+ r#"{"servers":{"s1":{"command":"python","env":{"API_KEY":"secret"}}}}"# ;
213+ let out = build_session_policy_json (
214+ "s-test" . to_string ( ) ,
215+ & Some ( vec ! [ "fs_read" . to_string( ) , "fs_write" . to_string( ) ] ) ,
216+ & None ,
217+ & skill_policy,
218+ Some ( mcp_config_json) ,
219+ true ,
220+ )
221+ . unwrap ( ) ;
222+
223+ assert_eq ! ( out[ "session_id" ] . as_str( ) , Some ( "s-test" ) ) ;
224+ assert ! ( out. get( "allowed_tools" ) . is_some( ) ) ;
225+ assert ! ( out. get( "allowed_skills" ) . is_some( ) ) ;
226+ assert ! ( out. get( "skill_policy" ) . is_some( ) ) ;
227+ assert ! ( out. get( "mcp" ) . is_some( ) ) ;
228+
229+ let tools: Vec < String > = out[ "allowed_tools" ]
230+ . as_array ( )
231+ . unwrap ( )
232+ . iter ( )
233+ . filter_map ( |v| v. as_str ( ) . map ( |s| s. to_string ( ) ) )
234+ . collect ( ) ;
235+ assert ! ( tools. contains( & "fs_read" . to_string( ) ) ) ;
236+ assert ! ( tools. contains( & "fs_write" . to_string( ) ) ) ;
237+
238+ assert_eq ! (
239+ out[ "skill_policy" ] [ "require_approval" ] . as_bool( ) ,
240+ Some ( true )
241+ ) ;
242+ assert_eq ! (
243+ out[ "skill_policy" ] [ "auto_approve_readonly" ] . as_bool( ) ,
244+ Some ( false )
245+ ) ;
246+ assert_eq ! ( out[ "skill_policy" ] [ "allowlist" ] [ 0 ] . as_str( ) , Some ( "alpha" ) ) ;
247+
248+ assert_eq ! ( out[ "mcp" ] [ "present" ] . as_bool( ) , Some ( true ) ) ;
249+ let servers: Vec < String > = out[ "mcp" ] [ "servers" ]
250+ . as_array ( )
251+ . unwrap ( )
252+ . iter ( )
253+ . filter_map ( |v| v. as_str ( ) . map ( |s| s. to_string ( ) ) )
254+ . collect ( ) ;
255+ assert_eq ! ( servers, vec![ "s1" . to_string( ) ] ) ;
256+ assert_eq ! (
257+ out[ "mcp" ] [ "config" ] [ "servers" ] [ "s1" ] [ "env" ] [ "API_KEY" ] . as_str( ) ,
258+ Some ( "[redacted]" )
259+ ) ;
260+ assert_eq ! (
261+ out[ "mcp" ] [ "config" ] [ "servers" ] [ "s1" ] [ "command" ] . as_str( ) ,
262+ Some ( "python" )
263+ ) ;
264+ }
265+
266+ #[ test]
267+ fn build_session_policy_json_omits_sanitized_config_when_not_requested ( ) {
268+ let out = build_session_policy_json (
269+ "s-test" . to_string ( ) ,
270+ & None ,
271+ & None ,
272+ & rexos:: agent:: SessionSkillPolicy :: default ( ) ,
273+ Some ( r#"{"servers":{"s1":{"command":"python"}}}"# ) ,
274+ false ,
275+ )
276+ . unwrap ( ) ;
277+
278+ let mcp = out[ "mcp" ] . as_object ( ) . unwrap ( ) ;
279+ assert_eq ! ( mcp. get( "present" ) . and_then( |v| v. as_bool( ) ) , Some ( true ) ) ;
280+ assert ! ( mcp. contains_key( "servers" ) ) ;
281+ assert ! ( !mcp. contains_key( "config" ) ) ;
282+ }
283+
284+ #[ test]
285+ fn build_session_policy_json_reports_invalid_mcp_json ( ) {
286+ let out = build_session_policy_json (
287+ "s-test" . to_string ( ) ,
288+ & None ,
289+ & None ,
290+ & rexos:: agent:: SessionSkillPolicy :: default ( ) ,
291+ Some ( "not-json" ) ,
292+ true ,
293+ )
294+ . unwrap ( ) ;
295+
296+ let mcp = out[ "mcp" ] . as_object ( ) . unwrap ( ) ;
297+ assert_eq ! ( mcp. get( "present" ) . and_then( |v| v. as_bool( ) ) , Some ( true ) ) ;
298+ assert ! ( mcp. get( "servers" ) . is_none( ) ) ;
299+ assert ! ( mcp. get( "config" ) . is_none( ) ) ;
300+ assert ! ( mcp
301+ . get( "parse_error" )
302+ . and_then( |v| v. as_str( ) )
303+ . unwrap_or( "" )
304+ . starts_with( "invalid JSON:" ) , ) ;
305+ }
306+
178307 #[ test]
179308 fn sanitize_mcp_config_redacts_env_values ( ) {
180309 let input = serde_json:: json!( {
0 commit comments