@@ -7,12 +7,21 @@ import type { ChatMessage } from '@/app/workspace/[workspaceId]/home/types'
77import {
88 captureRevealedSimKeys ,
99 extractRevealedSimKeys ,
10+ extractRevealedSimKeysFromBlocks ,
1011 restoreRevealedSimKeysForMessage ,
12+ toolResultForModel ,
1113} from './sim-key-redaction'
1214
1315const credential = ( value : string ) =>
1416 `<credential>${ JSON . stringify ( { value, type : 'sim_key' } ) } </credential>`
1517const redacted = `<credential>${ JSON . stringify ( { type : 'sim_key' , redacted : true } ) } </credential>`
18+ // The value-less placeholder the model now emits (no `redacted` flag).
19+ const placeholder = `<credential>${ JSON . stringify ( { type : 'sim_key' } ) } </credential>`
20+
21+ const apiKeyBlock = ( key : string ) => ( {
22+ type : 'tool_call' as const ,
23+ toolCall : { name : 'generate_api_key' , result : { success : true , output : { id : 'k1' , key } } } ,
24+ } )
1625
1726describe ( 'sim-key-redaction' , ( ) => {
1827 describe ( 'extractRevealedSimKeys' , ( ) => {
@@ -28,6 +37,55 @@ describe('sim-key-redaction', () => {
2837 } )
2938 } )
3039
40+ describe ( 'toolResultForModel' , ( ) => {
41+ it ( 'reduces a successful generate_api_key result to only its status message' , ( ) => {
42+ const data = {
43+ id : 'k1' ,
44+ name : 'prod' ,
45+ key : 'sk-sim-secret' ,
46+ workspaceId : 'ws-1' ,
47+ message : 'API key "prod" created.' ,
48+ }
49+ expect ( toolResultForModel ( 'generate_api_key' , data ) ) . toBe ( 'API key "prod" created.' )
50+ } )
51+
52+ it ( 'leaves other tools untouched' , ( ) => {
53+ const data = { key : 'not-a-secret' , ok : true }
54+ expect ( toolResultForModel ( 'read' , data ) ) . toBe ( data )
55+ } )
56+
57+ it ( 'passes generate_api_key errors through (no key to withhold)' , ( ) => {
58+ const data = { error : 'name is required' }
59+ expect ( toolResultForModel ( 'generate_api_key' , data ) ) . toBe ( data )
60+ expect ( toolResultForModel ( 'generate_api_key' , undefined ) ) . toBe ( undefined )
61+ } )
62+ } )
63+
64+ describe ( 'extractRevealedSimKeysFromBlocks' , ( ) => {
65+ it ( 'pulls generate_api_key output keys in block order' , ( ) => {
66+ expect (
67+ extractRevealedSimKeysFromBlocks ( [ apiKeyBlock ( 'sk-sim-A' ) , apiKeyBlock ( 'sk-sim-B' ) ] )
68+ ) . toEqual ( [ 'sk-sim-A' , 'sk-sim-B' ] )
69+ } )
70+
71+ it ( 'skips redacted markers and unrelated tools' , ( ) => {
72+ const blocks = [
73+ apiKeyBlock ( '[REDACTED]' ) ,
74+ {
75+ type : 'tool_call' as const ,
76+ toolCall : { name : 'read' , result : { success : true , output : { key : 'sk-x' } } } ,
77+ } ,
78+ apiKeyBlock ( 'sk-sim-A' ) ,
79+ ]
80+ expect ( extractRevealedSimKeysFromBlocks ( blocks ) ) . toEqual ( [ 'sk-sim-A' ] )
81+ } )
82+
83+ it ( 'returns nothing for empty/undefined block lists' , ( ) => {
84+ expect ( extractRevealedSimKeysFromBlocks ( undefined ) ) . toEqual ( [ ] )
85+ expect ( extractRevealedSimKeysFromBlocks ( [ ] ) ) . toEqual ( [ ] )
86+ } )
87+ } )
88+
3189 describe ( 'captureRevealedSimKeys' , ( ) => {
3290 it ( 'records new keys under each provided key' , ( ) => {
3391 const cache = new Map < string , string [ ] > ( )
@@ -59,6 +117,21 @@ describe('sim-key-redaction', () => {
59117 captureRevealedSimKeys ( cache , [ 'msg-1' ] , 'plain assistant text' )
60118 expect ( cache . has ( 'msg-1' ) ) . toBe ( false )
61119 } )
120+
121+ it ( 'sources the key from the generate_api_key tool result (model text is a redacted placeholder)' , ( ) => {
122+ const cache = new Map < string , string [ ] > ( )
123+ captureRevealedSimKeys ( cache , [ 'msg-1' , 'req-1' ] , `Here is your key: ${ redacted } ` , [
124+ apiKeyBlock ( 'sk-sim-fromtool' ) ,
125+ ] )
126+ expect ( cache . get ( 'msg-1' ) ) . toEqual ( [ 'sk-sim-fromtool' ] )
127+ expect ( cache . get ( 'req-1' ) ) . toEqual ( [ 'sk-sim-fromtool' ] )
128+ } )
129+
130+ it ( 'prefers tool-result keys over any inline content values' , ( ) => {
131+ const cache = new Map < string , string [ ] > ( )
132+ captureRevealedSimKeys ( cache , [ 'msg-1' ] , credential ( 'sk-content' ) , [ apiKeyBlock ( 'sk-tool' ) ] )
133+ expect ( cache . get ( 'msg-1' ) ) . toEqual ( [ 'sk-tool' ] )
134+ } )
62135 } )
63136
64137 describe ( 'restoreRevealedSimKeysForMessage' , ( ) => {
@@ -76,6 +149,32 @@ describe('sim-key-redaction', () => {
76149 expect ( restored . contentBlocks ?. [ 0 ] . content ) . toContain ( '"sk-sim-A"' )
77150 } )
78151
152+ it ( 'fills a value-less {"type":"sim_key"} placeholder (no redacted flag needed)' , ( ) => {
153+ const cache = new Map < string , string [ ] > ( [ [ 'msg-1' , [ 'sk-sim-A' ] ] ] )
154+ const msg : ChatMessage = {
155+ id : 'msg-1' ,
156+ role : 'assistant' ,
157+ content : `Here is your key: ${ placeholder } save it.` ,
158+ contentBlocks : [ { type : 'text' , content : `Here is your key: ${ placeholder } save it.` } ] ,
159+ }
160+ const restored = restoreRevealedSimKeysForMessage ( msg , cache )
161+ expect ( restored . content ) . toContain ( '"sk-sim-A"' )
162+ expect ( restored . contentBlocks ?. [ 0 ] . content ) . toContain ( '"sk-sim-A"' )
163+ } )
164+
165+ it ( 'fills value-less and redacted placeholders positionally in one message' , ( ) => {
166+ const cache = new Map < string , string [ ] > ( [ [ 'msg-1' , [ 'sk-sim-A' , 'sk-sim-B' ] ] ] )
167+ const msg : ChatMessage = {
168+ id : 'msg-1' ,
169+ role : 'assistant' ,
170+ content : `first ${ placeholder } second ${ redacted } ` ,
171+ }
172+ const restored = restoreRevealedSimKeysForMessage ( msg , cache )
173+ expect ( restored . content ) . toBe (
174+ `first ${ credential ( 'sk-sim-A' ) } second ${ credential ( 'sk-sim-B' ) } `
175+ )
176+ } )
177+
79178 it ( 'substitutes multiple keys in stream order' , ( ) => {
80179 const cache = new Map < string , string [ ] > ( [ [ 'msg-1' , [ 'sk-sim-A' , 'sk-sim-B' ] ] ] )
81180 const msg : ChatMessage = {
0 commit comments