@@ -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 { gateAgentTools } 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 mcpClientTools(
3560 client : MCPClient ,
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
@@ -52,7 +77,30 @@ export function mcpClientTools(
5277 signal : AbortSignal ,
5378 ) : Promise < ToolResult > => {
5479 try {
55- const content = await client . call ( tool . name , call . arguments , signal ) ;
80+ const rawBlocks =
81+ typeof client . callBlocks === "function"
82+ ? await client . callBlocks ( tool . name , call . arguments , signal )
83+ : [
84+ {
85+ type : "text" ,
86+ text : await client . call ( tool . name , call . arguments , signal ) ,
87+ } satisfies MCPContentBlock ,
88+ ] ;
89+ const authorizedBlocks = applyPolicyToBlocks ( rawBlocks ) ;
90+ const archive = getEvidenceArchive ?.( ) ;
91+ if ( archive !== undefined ) {
92+ try {
93+ await archive . recordAuthorizedPayload ( {
94+ kind : "tool_result" ,
95+ payload : { blocks : authorizedBlocks } ,
96+ callId : call . id ,
97+ provenance : "mcp:post-policy-pre-flatten" ,
98+ } ) ;
99+ } catch {
100+ // Archive write must not fail a successful tool result.
101+ }
102+ }
103+ const flattened = unwrapToolContent ( authorizedBlocks ) ;
56104 const writeBlob = getBlobWriter ?.( ) ;
57105 const contextDir = getContextDir ?.( ) ;
58106 const spill =
@@ -63,14 +111,31 @@ export function mcpClientTools(
63111 ...( contextDir !== undefined ? { contextDir } : { } ) ,
64112 }
65113 : undefined ;
66- return {
67- callId : call . id ,
68- content : await sanitizeMcpResultContent ( content , spill ) ,
69- } ;
114+ const content = await truncateToolResultContent (
115+ flattened ,
116+ undefined ,
117+ spill ,
118+ ) ;
119+ return { callId : call . id , content } ;
70120 } catch ( err ) {
121+ const message = err instanceof Error ? err . message : String ( err ) ;
122+ const scrubbed = scrubSecretShapedContent ( message ) ;
123+ const archive = getEvidenceArchive ?.( ) ;
124+ if ( archive !== undefined ) {
125+ try {
126+ await archive . recordAuthorizedPayload ( {
127+ kind : "tool_result" ,
128+ payload : scrubbed ,
129+ callId : call . id ,
130+ provenance : "mcp:error" ,
131+ } ) ;
132+ } catch {
133+ // Archive write must not fail a successful tool result.
134+ }
135+ }
71136 return {
72137 callId : call . id ,
73- content : err instanceof Error ? err . message : String ( err ) ,
138+ content : scrubbed ,
74139 isError : true ,
75140 } ;
76141 }
0 commit comments