@@ -164,24 +164,23 @@ impl ClawMcpService {
164164 ) ;
165165 }
166166 }
167- if let Some ( summary) = raw_args
167+ // Scrub structural PII from any provided summary before it is persisted or
168+ // indexed for search; stored locally only, never sent to analytics. Last write wins.
169+ let scrubbed_summary = raw_args
168170 . get ( "summary" )
169171 . and_then ( Value :: as_str)
170172 . map ( str:: trim)
171173 . filter ( |summary| !summary. is_empty ( ) )
174+ . map ( scrub_summary) ;
175+ if let Some ( clean) = scrubbed_summary. as_deref ( )
176+ && !clean. is_empty ( )
177+ && let Err ( error) = self
178+ . state
179+ . audit_log
180+ . set_task_summary ( started. session . id ( ) . as_str ( ) , clean)
181+ . await
172182 {
173- // Scrub structural PII before it is persisted and indexed for audit search;
174- // stored locally only, never sent to analytics. Last write wins.
175- let clean = scrub_summary ( summary) ;
176- if !clean. is_empty ( )
177- && let Err ( error) = self
178- . state
179- . audit_log
180- . set_task_summary ( started. session . id ( ) . as_str ( ) , & clean)
181- . await
182- {
183- warn ! ( error = %error, "failed to store task summary" ) ;
184- }
183+ warn ! ( error = %error, "failed to store task summary" ) ;
185184 }
186185 let browser = self . state . browser . session ( ) . await ;
187186 apply_agent_tab_group_title (
@@ -193,13 +192,19 @@ impl ClawMcpService {
193192 )
194193 . await ;
195194 let result = ToolResult :: text ( rename. response , None ) ;
195+ // The audit dispatch persists the raw tool arguments; substitute the scrubbed
196+ // summary so the unsanitized text never reaches the audit detail timeline.
197+ let dispatch_args = match scrubbed_summary. as_deref ( ) {
198+ Some ( clean) => with_scrubbed_summary ( raw_args, clean) ,
199+ None => raw_args. clone ( ) ,
200+ } ;
196201 if let Err ( error) = record_local_tool_dispatch (
197202 & self . state ,
198203 LocalToolDispatch {
199204 session : & started. session ,
200205 agent_label : & started. agent_label ,
201206 tool_name : NAME_SESSION_TOOL_NAME ,
202- raw_args,
207+ raw_args : & dispatch_args ,
203208 result : & result,
204209 duration_ms : i64:: try_from ( started_at. elapsed ( ) . as_millis ( ) ) . unwrap_or ( i64:: MAX ) ,
205210 dispatch_id : dispatch_id. clone ( ) ,
@@ -703,6 +708,17 @@ fn scrub_summary(raw: &str) -> String {
703708 }
704709}
705710
711+ /// Clones the tool arguments with the `summary` field replaced by its already-scrubbed
712+ /// form, so the audit dispatch timeline persists the sanitized summary rather than the raw
713+ /// one the scrubber removed from `tasks.task_summary` and the search index.
714+ fn with_scrubbed_summary ( raw_args : & Value , clean : & str ) -> Value {
715+ let mut owned = raw_args. clone ( ) ;
716+ if let Some ( object) = owned. as_object_mut ( ) {
717+ object. insert ( "summary" . to_string ( ) , Value :: String ( clean. to_string ( ) ) ) ;
718+ }
719+ owned
720+ }
721+
706722fn is_pii_token ( token : & str ) -> bool {
707723 let lower = token. to_ascii_lowercase ( ) ;
708724 if token. contains ( '@' )
@@ -1298,6 +1314,20 @@ mod tests {
12981314 assert ! ( scrub_summary( & raw) . chars( ) . count( ) <= SUMMARY_MAX_LEN ) ;
12991315 }
13001316
1317+ #[ test]
1318+ fn with_scrubbed_summary_replaces_summary_and_keeps_other_args ( ) {
1319+ let raw = "Emailed john@acme.com the invoices" ;
1320+ let clean = scrub_summary ( raw) ;
1321+ let sanitized =
1322+ with_scrubbed_summary ( & json ! ( { "name" : "invoice sync" , "summary" : raw } ) , & clean) ;
1323+ // The recorded dispatch args carry the scrubbed copy, never the raw one.
1324+ assert_eq ! ( sanitized[ "summary" ] . as_str( ) , Some ( clean. as_str( ) ) ) ;
1325+ assert ! ( !clean. contains( '@' ) ) ;
1326+ assert ! ( !clean. to_ascii_lowercase( ) . contains( "acme.com" ) ) ;
1327+ // Unrelated arguments are preserved verbatim.
1328+ assert_eq ! ( sanitized[ "name" ] . as_str( ) , Some ( "invoice sync" ) ) ;
1329+ }
1330+
13011331 #[ tokio:: test]
13021332 async fn save_skill_is_registered_locally_with_annotations ( ) -> anyhow:: Result < ( ) > {
13031333 let call = crate :: api:: mcp:: test_support:: tool_call ( "tabs" , json ! ( { } ) ) . await ?;
0 commit comments