@@ -330,6 +330,61 @@ describe("mcpClientToAgentTools", () => {
330330 expect ( detailJson ) . not . toContain ( "sk-live-" ) ;
331331 } ) ;
332332
333+ test ( "scrubs short credential-keyed values from every retained surface" , async ( ) => {
334+ const { archive, blobs } = memoryEvidenceArchive ( ) ;
335+ const result = await runEnvelopeTool (
336+ {
337+ blocks : [ { type : "resource" , authorization : "block-short" } ] ,
338+ isError : false ,
339+ structuredContent : {
340+ apiKey : "top-short" ,
341+ nested : { auth : "nested-short" , access_token : "access-short" } ,
342+ } ,
343+ } ,
344+ "c-mcp-short-credential-values" ,
345+ { getEvidenceArchive : ( ) => archive } ,
346+ ) ;
347+
348+ const surfaces = [
349+ JSON . stringify ( result . detail ) ,
350+ String ( result . content ) ,
351+ serializePersistedToolResultTurn ( result ) ,
352+ ...[ ...blobs . values ( ) ] . map ( ( bytes ) => new TextDecoder ( ) . decode ( bytes ) ) ,
353+ ] ;
354+ for ( const surface of surfaces ) {
355+ expect ( surface ) . toContain ( CREDENTIAL_REDACTION ) ;
356+ expect ( surface ) . not . toContain ( "top-short" ) ;
357+ expect ( surface ) . not . toContain ( "nested-short" ) ;
358+ expect ( surface ) . not . toContain ( "access-short" ) ;
359+ expect ( surface ) . not . toContain ( "block-short" ) ;
360+ }
361+ } ) ;
362+
363+ test ( "preserves special structured keys without prototype mutation" , async ( ) => {
364+ const { archive } = memoryEvidenceArchive ( ) ;
365+ const structuredContent = JSON . parse (
366+ '{"__proto__":"top","constructor":"ctor","nested":{"__proto__":"nested"}}' ,
367+ ) as Record < string , unknown > ;
368+ const result = await runEnvelopeTool (
369+ { blocks : [ ] , structuredContent } ,
370+ "c-mcp-special-keys" ,
371+ { getEvidenceArchive : ( ) => archive } ,
372+ ) ;
373+
374+ const detail = result . detail as Record < string , unknown > ;
375+ const nested = detail . nested as Record < string , unknown > ;
376+ expect ( Object . getPrototypeOf ( detail ) ) . toBeNull ( ) ;
377+ expect ( Object . getPrototypeOf ( nested ) ) . toBeNull ( ) ;
378+ expect ( JSON . stringify ( detail ) ) . toBe ( JSON . stringify ( structuredContent ) ) ;
379+ expect ( ( { } as Record < string , unknown > ) . top ) . toBeUndefined ( ) ;
380+
381+ const [ occurrence ] = await archive . listOccurrences ( ) ;
382+ if ( occurrence === undefined ) throw new Error ( "missing archive occurrence" ) ;
383+ expect (
384+ await archive . readAuthorizedPayload ( occurrence . occurrenceId ) ,
385+ ) . toContain ( '"__proto__":"top"' ) ;
386+ } ) ;
387+
333388 test ( "scrubs structured keys from detail, archive bytes, and model content" , async ( ) => {
334389 const topLevelKey = [ "sk-" , "live-" , "a" . repeat ( 24 ) ] . join ( "" ) ;
335390 const nestedKey = [ "sk-" , "live-" , "b" . repeat ( 24 ) ] . join ( "" ) ;
@@ -370,21 +425,28 @@ describe("mcpClientToAgentTools", () => {
370425
371426 test ( "keeps oversized structured content full only in the evidence archive" , async ( ) => {
372427 const { archive } = memoryEvidenceArchive ( ) ;
428+ const store = fakeBlobStore ( ) ;
373429 const hugeValue = "x" . repeat ( MAX_RESULT_CHARS * 4 ) ;
430+ const callId = "c-mcp-oversized-detail" ;
374431 const result = await runEnvelopeTool (
375432 {
376433 blocks : [ ] ,
377434 isError : false ,
378435 structuredContent : { hugeValue } ,
379436 } ,
380- "c-mcp-oversized-detail" ,
381- { getEvidenceArchive : ( ) => archive } ,
437+ callId ,
438+ {
439+ getEvidenceArchive : ( ) => archive ,
440+ getBlobWriter : ( ) => store . writeBlob ,
441+ } ,
382442 ) ;
383443
384444 expect ( result . detail ) . toBeUndefined ( ) ;
385445 expect ( JSON . stringify ( result ) . length ) . toBeLessThanOrEqual (
386446 MAX_RESULT_CHARS + 256 ,
387447 ) ;
448+ expect ( store . blobs . has ( spillBlobKey ( callId ) ) ) . toBe ( false ) ;
449+ expect ( result . content ) . not . toContain ( "tool-output:///" ) ;
388450 const serializedTurn = serializePersistedToolResultTurn ( result ) ;
389451 expect ( serializedTurn . length ) . toBeLessThanOrEqual ( MAX_RESULT_CHARS + 512 ) ;
390452 expect ( serializedTurn ) . not . toContain ( hugeValue ) ;
@@ -397,20 +459,83 @@ describe("mcpClientToAgentTools", () => {
397459 expect ( archived . structuredContent . hugeValue ) . toBe ( hugeValue ) ;
398460 } ) ;
399461
400- test ( "omits unserializable structured detail without failing text content" , async ( ) => {
462+ test ( "spills oversized structured content when the evidence archive fails" , async ( ) => {
463+ const { archive } = memoryEvidenceArchive ( ) ;
464+ const failingArchive = {
465+ ...archive ,
466+ recordAuthorizedPayload : async ( ) => {
467+ throw new Error ( "archive unavailable" ) ;
468+ } ,
469+ } ;
470+ const store = fakeBlobStore ( ) ;
471+ const hugeValue = "y" . repeat ( MAX_RESULT_CHARS * 4 ) ;
472+ const callId = "c-mcp-oversized-archive-failure" ;
473+
401474 const result = await runEnvelopeTool (
475+ { blocks : [ ] , structuredContent : { hugeValue } } ,
476+ callId ,
402477 {
403- blocks : [ { type : "text" , text : "usable text" } ] ,
404- isError : false ,
405- structuredContent : { unsupported : 1n } ,
478+ getEvidenceArchive : ( ) => failingArchive ,
479+ getBlobWriter : ( ) => store . writeBlob ,
406480 } ,
407- "c-mcp-unserializable-detail" ,
408481 ) ;
409482
410- expect ( result . isError ) . toBeUndefined ( ) ;
411- expect ( result . content ) . toBe ( "usable text" ) ;
483+ const spill = store . blobs . get ( spillBlobKey ( callId ) ) ;
484+ expect ( spill ) . toBeDefined ( ) ;
485+ expect ( new TextDecoder ( ) . decode ( defined ( spill ) . bytes ) ) . toContain ( hugeValue ) ;
486+ expect ( result . content ) . toContain ( `tool-output:///${ spillBlobKey ( callId ) } ` ) ;
412487 expect ( result . detail ) . toBeUndefined ( ) ;
413- expect ( JSON . stringify ( result ) . length ) . toBeLessThan ( MAX_RESULT_CHARS ) ;
488+ } ) ;
489+
490+ test ( "rejects non-JSON structured values without invoking hooks or leaking" , async ( ) => {
491+ const leakMarker = "short-private-marker" ;
492+ let accessorReads = 0 ;
493+ let toJSONCalls = 0 ;
494+ const accessor = Object . defineProperty ( { } , "value" , {
495+ enumerable : true ,
496+ get : ( ) => {
497+ accessorReads ++ ;
498+ return leakMarker ;
499+ } ,
500+ } ) ;
501+ const customJSON = {
502+ toJSON : ( ) => {
503+ toJSONCalls ++ ;
504+ return { leaked : leakMarker } ;
505+ } ,
506+ } ;
507+ const cycle : Record < string , unknown > = { } ;
508+ cycle . self = cycle ;
509+ const invalidValues : unknown [ ] = [
510+ accessor ,
511+ customJSON ,
512+ { value : ( ) => leakMarker } ,
513+ { value : Symbol ( leakMarker ) } ,
514+ { value : 1n } ,
515+ cycle ,
516+ ] ;
517+
518+ for ( const [ index , structuredContent ] of invalidValues . entries ( ) ) {
519+ const { archive, blobs } = memoryEvidenceArchive ( ) ;
520+ const result = await runEnvelopeTool (
521+ {
522+ blocks : [ { type : "text" , text : "usable text" } ] ,
523+ structuredContent : structuredContent as Record < string , unknown > ,
524+ } ,
525+ `c-mcp-invalid-structured-${ index } ` ,
526+ { getEvidenceArchive : ( ) => archive } ,
527+ ) ;
528+
529+ expect ( result . isError ) . toBe ( true ) ;
530+ expect ( result . detail ) . toBeUndefined ( ) ;
531+ expect ( result . content ) . toBe ( "Tool result is not JSON-safe" ) ;
532+ expect ( JSON . stringify ( result ) ) . not . toContain ( leakMarker ) ;
533+ for ( const bytes of blobs . values ( ) ) {
534+ expect ( new TextDecoder ( ) . decode ( bytes ) ) . not . toContain ( leakMarker ) ;
535+ }
536+ }
537+ expect ( accessorReads ) . toBe ( 0 ) ;
538+ expect ( toJSONCalls ) . toBe ( 0 ) ;
414539 } ) ;
415540
416541 test ( "archives identical success and failure payloads with distinct isError" , async ( ) => {
0 commit comments