@@ -2,18 +2,43 @@ import type { AgentTool } from "@intx/agent";
22import type { ToolCall , ToolResult } from "@intx/types/runtime" ;
33import type { PermissionGate } from "../permission/gate.js" ;
44import { gateToolCall } from "../plugins/permission-plugin.js" ;
5- import { scrubSecretShapedContent } from "../plugins/tool-result-secret-scrub.js" ;
5+ import {
6+ scrubSecretShapedContent ,
7+ scrubSecretShapedValue ,
8+ } from "../plugins/tool-result-secret-scrub.js" ;
69import {
710 truncateToolResultContent ,
811 type SpillBlobWriter ,
912} from "../plugins/result-truncation-plugin.js" ;
10- import type { MCPClient } from "./client.js" ;
13+ import type { CompactionArchive } from "../session/compaction-archive.js" ;
14+ import type { MCPClient , MCPContentBlock } from "./client.js" ;
1115import { mcpToolName } from "./tool-name.js" ;
16+ import { unwrapToolContent } from "./client.js" ;
1217
1318export interface McpSpillOptions {
1419 getBlobWriter ?: ( ) => SpillBlobWriter | undefined ;
1520 getContextDir ?: ( ) => string | undefined ;
1621 excludeToolNames ?: readonly string [ ] ;
22+ /** Primary-only evidence archive; workers omit this getter. */
23+ getEvidenceArchive ?: ( ) => CompactionArchive | undefined ;
24+ }
25+
26+ function applyPolicyToBlocks ( blocks : MCPContentBlock [ ] ) : MCPContentBlock [ ] {
27+ return blocks . map ( ( block ) => {
28+ const next = { ...block } ;
29+ if ( typeof next . text === "string" ) {
30+ next . text = scrubSecretShapedContent ( next . text ) ;
31+ }
32+ for ( const [ key , value ] of Object . entries ( next ) ) {
33+ if ( key === "type" || key === "text" ) continue ;
34+ if ( typeof value === "string" ) {
35+ next [ key ] = scrubSecretShapedContent ( value ) ;
36+ } else if ( value !== null && typeof value === "object" ) {
37+ next [ key ] = scrubSecretShapedValue ( value ) ;
38+ }
39+ }
40+ return next ;
41+ } ) ;
1742}
1843
1944// MCP results never reach the posix runner, so the secret-scrub and truncation
@@ -35,7 +60,7 @@ export function mcpClientToAgentTools(
3560 gate : PermissionGate ,
3661 spillOptions : McpSpillOptions = { } ,
3762) : AgentTool [ ] {
38- const { getBlobWriter, getContextDir, excludeToolNames = [ ] } = spillOptions ;
63+ const { getBlobWriter, getContextDir, excludeToolNames = [ ] , getEvidenceArchive } = spillOptions ;
3964 const excluded = new Set ( excludeToolNames ) ;
4065
4166 return client . tools
@@ -50,7 +75,26 @@ export function mcpClientToAgentTools(
5075 handler : ( call : ToolCall , signal : AbortSignal ) : Promise < ToolResult > =>
5176 gateToolCall ( gate , call , signal , async ( ) => {
5277 try {
53- const content = await client . call ( tool . name , call . arguments , signal ) ;
78+ const rawBlocks =
79+ typeof client . callBlocks === "function"
80+ ? await client . callBlocks ( tool . name , call . arguments , signal )
81+ : [
82+ {
83+ type : "text" ,
84+ text : await client . call ( tool . name , call . arguments , signal ) ,
85+ } satisfies MCPContentBlock ,
86+ ] ;
87+ const authorizedBlocks = applyPolicyToBlocks ( rawBlocks ) ;
88+ const archive = getEvidenceArchive ?.( ) ;
89+ if ( archive !== undefined ) {
90+ await archive . recordAuthorizedPayload ( {
91+ kind : "tool_result" ,
92+ payload : { blocks : authorizedBlocks } ,
93+ callId : call . id ,
94+ provenance : "mcp:post-policy-pre-flatten" ,
95+ } ) ;
96+ }
97+ const flattened = unwrapToolContent ( authorizedBlocks ) ;
5498 const writeBlob = getBlobWriter ?.( ) ;
5599 const contextDir = getContextDir ?.( ) ;
56100 const spill =
@@ -61,14 +105,30 @@ export function mcpClientToAgentTools(
61105 ...( contextDir !== undefined ? { contextDir } : { } ) ,
62106 }
63107 : undefined ;
64- return { callId : call . id , content : await sanitizeMcpResultContent ( content , spill ) } ;
108+ // Content is already scrubbed; truncate only (avoid double-scrub).
109+ const content = await truncateToolResultContent ( flattened , undefined , spill ) ;
110+ return { callId : call . id , content } ;
65111 } catch ( err ) {
112+ const message = err instanceof Error ? err . message : String ( err ) ;
113+ const scrubbed = scrubSecretShapedContent ( message ) ;
114+ const archive = getEvidenceArchive ?.( ) ;
115+ if ( archive !== undefined ) {
116+ await archive . recordAuthorizedPayload ( {
117+ kind : "tool_result" ,
118+ payload : scrubbed ,
119+ callId : call . id ,
120+ provenance : "mcp:error" ,
121+ } ) ;
122+ }
66123 return {
67124 callId : call . id ,
68- content : err instanceof Error ? err . message : String ( err ) ,
125+ content : scrubbed ,
69126 isError : true ,
70127 } ;
71128 }
72129 } ) ,
73130 } ) ) ;
74131}
132+
133+ // Keep sanitize helper exported for tests that still call the string path.
134+ export { sanitizeMcpResultContent } ;
0 commit comments