Skip to content

Commit eeb1001

Browse files
icecrasher321claude
andcommitted
fix(provenance): make one length floor the whole substitution rule
A literal shorter than eight characters is no longer substituted anywhere. It was already the floor for matches inside a larger token; below it a second tier still substituted whenever the hit sat on a word boundary — standing alone, delimited, or as the whole value — on the theory that those positions made the hit unambiguous. Position is not the variable that matters. A hit on `7` is uninformative wherever it sits, because the value space is ten. That tier rewrote `_raw_idx = 7` into `_raw_idx = {{WEEKLY_OWNWORK_TTL}}` for the one shard whose index collided with a TTL variable, and turned 2,000 boolean `had_error` cells into `[REDACTED_SECRET]` because a `*_ENABLED` variable held `false`. Each was patched with a per-value exception list; the floor subsumes both, so the lists are deleted. With no literal below the floor reaching a matcher, the tier's machinery is unreachable and goes with it: the match-policy type, its classifier, the word boundary test, and the detect/render mode that existed only to select between them. One constant now governs the question. The cost is explicit and accepted: a secret shorter than eight characters is no longer redacted from logs or model-visible content. Substitution cannot hide a value that short — an observer who can read the surrounding text can enumerate it. Two tests that pinned the old tier are rewritten to pin this, rather than deleted, so the trade stays visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fee45e2 commit eeb1001

20 files changed

Lines changed: 281 additions & 631 deletions

apps/sim/executor/execution/block-executor.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,15 +530,15 @@ describe('BlockExecutor', () => {
530530
const resolver = new VariableResolver(workflow, {}, state)
531531
const onBlockComplete = vi.fn(async () => {})
532532
const registry = new ResolvedSecretTraceRegistry([
533-
{ name: 'SHORT_SECRET', plaintext: 'Test', encryptedValue: 'encrypted-test' },
533+
{ name: 'SHORT_SECRET', plaintext: 'TestValue', encryptedValue: 'encrypted-test' },
534534
])
535535
const handler: BlockHandler = {
536536
canHandle: () => true,
537537
execute: async (blockContext, block) => {
538538
if (block.id === secretBlock.id) {
539-
blockContext.resolvedSecretTraceRegistry?.recordResolved('SHORT_SECRET', 'Test')
539+
blockContext.resolvedSecretTraceRegistry?.recordResolved('SHORT_SECRET', 'TestValue')
540540
}
541-
return { result: 'Test' }
541+
return { result: 'TestValue' }
542542
},
543543
}
544544
const executor = new BlockExecutor([handler], resolver, { onBlockComplete }, state)

apps/sim/executor/handlers/agent/memory.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,14 +347,14 @@ describe('Memory', () => {
347347
expect(result.content).toBe('foreign-secret')
348348
})
349349

350-
it.each(['123'])(
350+
it.each(['12345678'])(
351351
'projects short secret %s only in model text and arguments',
352352
async (secret) => {
353353
const registry = new ResolvedSecretTraceRegistry([
354354
{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' },
355355
])
356356
registry.recordResolved('TOKEN', secret)
357-
const converted = secret === '123' ? 123 : true
357+
const converted = secret === '12345678' ? 12345678 : true
358358
const message: Message = {
359359
role: 'assistant',
360360
content: `Result: ${secret}`,

apps/sim/executor/handlers/pi/search/tool.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function executionContext(
3636
const ctx = executionContext()
3737

3838
function buildTool(provider: 'exa' | 'serper' | 'parallel' | 'firecrawl' = 'exa', context = ctx) {
39-
return buildPiSearchToolSpec(context, { provider, apiKey: 'key-123' }, 'local')
39+
return buildPiSearchToolSpec(context, { provider, apiKey: 'key-1234567' }, 'local')
4040
}
4141

4242
async function run(
@@ -69,7 +69,7 @@ describe('buildPiSearchToolSpec', () => {
6969

7070
const [toolId, params, options] = mockExecuteTool.mock.calls[0]
7171
expect(toolId).toBe('exa_search')
72-
expect(params.apiKey).toBe('key-123')
72+
expect(params.apiKey).toBe('key-1234567')
7373
expect(params.timeout).toBe(10_000)
7474
expect(options.executionContext).toBe(ctx)
7575
expect(options.resolvedSecretTraceRegistry).toBeInstanceOf(ResolvedSecretTraceRegistry)
@@ -83,7 +83,7 @@ describe('buildPiSearchToolSpec', () => {
8383

8484
const [toolId, params] = mockExecuteTool.mock.calls[0]
8585
expect(toolId).toBe('serper_search')
86-
expect(params).toEqual({ query: 'pi', num: 2, apiKey: 'key-123', timeout: 10_000 })
86+
expect(params).toEqual({ query: 'pi', num: 2, apiKey: 'key-1234567', timeout: 10_000 })
8787
})
8888

8989
it('normalizes a successful provider response into the envelope', async () => {
@@ -193,17 +193,17 @@ describe('buildPiSearchToolSpec', () => {
193193

194194
it('projects only the exact resolver-recorded search key and leaves the raw result unchanged', async () => {
195195
const registry = new ResolvedSecretTraceRegistry([
196-
{ name: 'SEARCH_KEY', plaintext: 'key-123', encryptedValue: 'search-ciphertext' },
196+
{ name: 'SEARCH_KEY', plaintext: 'key-1234567', encryptedValue: 'search-ciphertext' },
197197
{ name: 'UNRELATED', plaintext: 'Test', encryptedValue: 'unrelated-ciphertext' },
198198
])
199-
registry.recordResolvedAtInputPath('SEARCH_KEY', 'key-123', ['searchApiKey'])
200-
registry.recordResolvedInputProjection(['searchApiKey'], 'key-123', '{{SEARCH_KEY}}')
199+
registry.recordResolvedAtInputPath('SEARCH_KEY', 'key-1234567', ['searchApiKey'])
200+
registry.recordResolvedInputProjection(['searchApiKey'], 'key-1234567', '{{SEARCH_KEY}}')
201201
registry.recordResolvedAtInputPath('UNRELATED', 'Test', ['task'])
202202
registry.recordResolvedInputProjection(['task'], 'Test', '{{UNRELATED}}')
203203
const output = {
204204
results: [
205205
{
206-
title: 'key-123',
206+
title: 'key-1234567',
207207
url: 'https://example.com/docs',
208208
text: 'Test',
209209
},
@@ -213,7 +213,7 @@ describe('buildPiSearchToolSpec', () => {
213213

214214
const result = await buildPiSearchToolSpec(
215215
executionContext(registry),
216-
{ provider: 'exa', apiKey: 'key-123' },
216+
{ provider: 'exa', apiKey: 'key-1234567' },
217217
'local',
218218
'{{SEARCH_KEY}}'
219219
).execute({ query: 'pi' })
@@ -231,7 +231,7 @@ describe('buildPiSearchToolSpec', () => {
231231
expect(output).toEqual({
232232
results: [
233233
{
234-
title: 'key-123',
234+
title: 'key-1234567',
235235
url: 'https://example.com/docs',
236236
text: 'Test',
237237
},

apps/sim/executor/utils/resolved-secret-content-projection.test.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -161,37 +161,37 @@ describe('projectResolvedSecretModelContent', () => {
161161

162162
it('keeps longest-match semantics when a known opaque placeholder is nested in a secret', () => {
163163
const registry = new ResolvedSecretTraceRegistry([
164-
{ name: 'Test', plaintext: 'Test', encryptedValue: 'test-ciphertext' },
164+
{ name: 'TestName', plaintext: 'TestName', encryptedValue: 'test-ciphertext' },
165165
{
166166
name: 'COMPOSITE',
167-
plaintext: 'x{{Test}}y',
167+
plaintext: 'x{{TestName}}y',
168168
encryptedValue: 'composite-ciphertext',
169169
},
170170
])
171-
registry.recordResolved('Test', 'Test')
172-
registry.recordResolved('COMPOSITE', 'x{{Test}}y')
171+
registry.recordResolved('TestName', 'TestName')
172+
registry.recordResolved('COMPOSITE', 'x{{TestName}}y')
173173

174-
expect(projectResolvedSecretModelContent('x{{Test}}y', registry)).toEqual({
174+
expect(projectResolvedSecretModelContent('x{{TestName}}y', registry)).toEqual({
175175
safe: true,
176176
value: '{{COMPOSITE}}',
177177
})
178178
})
179179

180180
it('projects exact typed numeric secrets, leaving booleans and null identifying nothing', () => {
181181
const registry = new ResolvedSecretTraceRegistry([
182-
{ name: 'NUMBER', plaintext: '123', encryptedValue: 'number-ciphertext' },
182+
{ name: 'NUMBER', plaintext: '12345678', encryptedValue: 'number-ciphertext' },
183183
{ name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'boolean-ciphertext' },
184184
{ name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' },
185185
])
186-
registry.recordResolved('NUMBER', '123')
186+
registry.recordResolved('NUMBER', '12345678')
187187
registry.recordResolved('BOOLEAN', 'true')
188188
registry.recordResolved('NULL', 'null')
189189

190190
expect(
191191
projectResolvedSecretModelContent(
192192
{
193-
strings: ['123', 'true', 'null'],
194-
number: 123,
193+
strings: ['12345678', 'true', 'null'],
194+
number: 12345678,
195195
boolean: true,
196196
nothing: null,
197197
unrelatedNumber: 1234,
@@ -212,12 +212,12 @@ describe('projectResolvedSecretModelContent', () => {
212212
})
213213
})
214214

215-
it.each(['123'])('keeps projected JSON argument strings valid (%s)', (secret) => {
215+
it.each(['12345678'])('keeps projected JSON argument strings valid (%s)', (secret) => {
216216
const registry = new ResolvedSecretTraceRegistry([
217217
{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' },
218218
])
219219
registry.recordResolved('TOKEN', secret)
220-
const typedValue = secret === '123' ? 123 : true
220+
const typedValue = secret === '12345678' ? 12345678 : true
221221

222222
const projection = projectResolvedSecretModelJsonStrings(
223223
[JSON.stringify({ secret, converted: typedValue, nested: [typedValue] })],
@@ -255,37 +255,37 @@ describe('projectResolvedSecretModelContent', () => {
255255

256256
it('is stable when a secret literal overlaps its own provenance alias', () => {
257257
const registry = new ResolvedSecretTraceRegistry([
258-
{ name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' },
258+
{ name: 'TOKEN', plaintext: 'TOKENTOKEN', encryptedValue: 'ciphertext' },
259259
])
260-
registry.recordResolved('TOKEN', 'TOKEN')
260+
registry.recordResolved('TOKEN', 'TOKENTOKEN')
261261

262-
const first = projectResolvedSecretModelContent('Bearer TOKEN', registry)
262+
const first = projectResolvedSecretModelContent('Bearer TOKENTOKEN', registry)
263263
expect(first).toEqual({ safe: true, value: 'Bearer {{TOKEN}}' })
264264
if (!first.safe) return
265265
expect(projectResolvedSecretModelContent(first.value, registry)).toEqual(first)
266266
})
267267

268268
it('preserves the canonical provenance label when its name equals the secret plaintext', () => {
269269
const registry = new ResolvedSecretTraceRegistry([
270-
{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' },
270+
{ name: 'TestName', plaintext: 'TestName', encryptedValue: 'ciphertext' },
271271
])
272-
registry.recordResolved('Test', 'Test')
272+
registry.recordResolved('TestName', 'TestName')
273273

274274
expect(
275275
projectResolvedSecretModelContent(
276276
{
277-
result: 'Test',
278-
source: 'return {{Test}}',
279-
error: "NameError: name 'Test' is not defined",
277+
result: 'TestName',
278+
source: 'return {{TestName}}',
279+
error: "NameError: name 'TestName' is not defined",
280280
},
281281
registry
282282
)
283283
).toEqual({
284284
safe: true,
285285
value: {
286-
result: '{{Test}}',
287-
source: 'return {{Test}}',
288-
error: "NameError: name '{{Test}}' is not defined",
286+
result: '{{TestName}}',
287+
source: 'return {{TestName}}',
288+
error: "NameError: name '{{TestName}}' is not defined",
289289
},
290290
})
291291
})
@@ -308,7 +308,7 @@ describe('projectResolvedSecretModelContent', () => {
308308
complete: true,
309309
matches: [
310310
{
311-
plaintext: 'x'.repeat(64 * 1024),
311+
plaintext: 'xxxxxxxx'.repeat(64 * 1024),
312312
replacement: '[REDACTED_SECRET]',
313313
},
314314
],
@@ -321,16 +321,18 @@ describe('projectResolvedSecretModelContent', () => {
321321

322322
it('keeps provenance-shaped content deterministic without trusting it as a protocol handle', () => {
323323
const registry = new ResolvedSecretTraceRegistry([
324-
{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' },
324+
{ name: 'TestName', plaintext: 'TestName', encryptedValue: 'ciphertext' },
325325
])
326-
registry.recordResolved('Test', 'Test')
326+
registry.recordResolved('TestName', 'TestName')
327327

328-
expect(projectResolvedSecretModelContent('{{Test}}', registry)).toEqual({
328+
expect(projectResolvedSecretModelContent('{{TestName}}', registry)).toEqual({
329329
safe: true,
330-
value: '{{Test}}',
330+
value: '{{TestName}}',
331331
})
332-
expect(isResolvedSecretModelContentUnchanged('{{Test}}', registry)).toBe(false)
333-
expect(isResolvedSecretModelContentUnchanged(['resource', '{{Test}}'], registry)).toBe(false)
332+
expect(isResolvedSecretModelContentUnchanged('{{TestName}}', registry)).toBe(false)
333+
expect(isResolvedSecretModelContentUnchanged(['resource', '{{TestName}}'], registry)).toBe(
334+
false
335+
)
334336
expect(isResolvedSecretModelContentUnchanged(['resource', 'safe'], registry)).toBe(true)
335337
})
336338
})
@@ -409,7 +411,7 @@ describe('projectResolvedSecretModelJsonContent', () => {
409411

410412
it('enforces the byte limit after secret aliases are projected', () => {
411413
const registry = new ResolvedSecretTraceRegistry([
412-
{ name: 'X', plaintext: 'x', encryptedValue: 'ciphertext' },
414+
{ name: 'X', plaintext: 'xxxxxxxx', encryptedValue: 'ciphertext' },
413415
])
414416
registry.recordResolved('X', 'x')
415417

@@ -445,7 +447,7 @@ describe('projectResolvedSecretDiagnosticError', () => {
445447

446448
it('sanitizes an inactive compiler alias without activating or scanning its secret', () => {
447449
const registry = new ResolvedSecretTraceRegistry([
448-
{ name: 'X', plaintext: 'x', encryptedValue: 'ciphertext' },
450+
{ name: 'X', plaintext: 'xxxxxxxx', encryptedValue: 'ciphertext' },
449451
])
450452
const error = new Error('Box __var_X')
451453

apps/sim/executor/utils/resolved-secret-content-projection.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ function createResolvedSecretModelMatcher(
3838
): ResolvedSecretMatcher | undefined {
3939
const matcher = createResolvedSecretMatcher(matches, {
4040
preserveNamedProvenanceLabels: true,
41-
mode: 'render',
4241
})
4342
if (!matcher) return undefined
4443

@@ -75,7 +74,7 @@ function createResolvedSecretModelMatcher(
7574
})),
7675
...opaquePlaceholderMatches,
7776
],
78-
{ preserveNamedProvenanceLabels: true, mode: 'render' }
77+
{ preserveNamedProvenanceLabels: true }
7978
)
8079
}
8180

0 commit comments

Comments
 (0)