Skip to content

Commit 8f0cd3c

Browse files
committed
fix(provenance): record why a resolved-secret registry became incomplete
Incompleteness is one-way: once any guard trips, every later model projection in the run fails and the user is left with a single opaque sentence. Every guard could set it and none recorded which, in a file that imported no logger at all, so the cause could not be recovered after the fact. Name each guard with a static reason literal. Originating causes log at error because they permanently fail the run and error is the only level that survives every default the logger falls back to; reasons that merely carry an upstream fault forward log at warn so one fault does not read as several. The decrypt catch no longer discards its cause. No behaviour change. Reasons are static literals and the logged input path is block/field names; no resolved value is recorded.
1 parent cdeb83d commit 8f0cd3c

2 files changed

Lines changed: 196 additions & 26 deletions

File tree

apps/sim/executor/utils/resolved-secret-trace-registry.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22

3-
const { mockDecryptSecret } = vi.hoisted(() => ({
3+
const { mockDecryptSecret, mockLogger } = vi.hoisted(() => ({
44
mockDecryptSecret: vi.fn(),
5+
mockLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
56
}))
67

78
vi.mock('@/lib/core/security/encryption', () => ({
89
decryptSecret: mockDecryptSecret,
910
}))
1011

12+
vi.mock('@sim/logger', () => ({
13+
createLogger: () => mockLogger,
14+
}))
15+
1116
import {
1217
ANONYMOUS_SECRET_TRACE_REPLACEMENT,
1318
createResolvedSecretTraceRegistry,
@@ -1279,3 +1284,88 @@ describe('ResolvedSecretTraceRegistry', () => {
12791284
expect(registry.getModelEgressSnapshot()).toEqual({ complete: false })
12801285
})
12811286
})
1287+
1288+
describe('incompleteness diagnostics', () => {
1289+
const scope = { userId: 'user-1', workspaceId: 'workspace-1' }
1290+
1291+
beforeEach(() => {
1292+
mockLogger.warn.mockClear()
1293+
mockLogger.error.mockClear()
1294+
})
1295+
1296+
it('reports an originating incompleteness at error so the default log level cannot hide it', () => {
1297+
const registry = new ResolvedSecretTraceRegistry([], scope)
1298+
1299+
registry.markIncomplete('projection-mismatch')
1300+
1301+
expect(mockLogger.warn).not.toHaveBeenCalled()
1302+
expect(mockLogger.error).toHaveBeenCalledWith(
1303+
'Resolved secret registry marked incomplete',
1304+
expect.objectContaining({ reason: 'projection-mismatch' })
1305+
)
1306+
})
1307+
1308+
it('reports an inherited incompleteness at warn so one fault does not read as several', () => {
1309+
const registry = new ResolvedSecretTraceRegistry([], scope)
1310+
1311+
registry.markIncomplete('inherited-incomplete-source')
1312+
1313+
expect(mockLogger.error).not.toHaveBeenCalled()
1314+
expect(mockLogger.warn).toHaveBeenCalledWith(
1315+
'Resolved secret registry marked incomplete',
1316+
expect.objectContaining({ reason: 'inherited-incomplete-source' })
1317+
)
1318+
})
1319+
1320+
it('names the guard that tripped rather than reporting unspecified', () => {
1321+
const registry = new ResolvedSecretTraceRegistry([], scope)
1322+
1323+
registry.recordResolved('MISSING', 'value-not-in-catalog')
1324+
1325+
expect(mockLogger.error).toHaveBeenCalledWith(
1326+
'Resolved secret registry marked incomplete',
1327+
expect.objectContaining({ reason: 'unverified-resolved-entry' })
1328+
)
1329+
})
1330+
1331+
it('separates a tool-call scope mismatch from a merged child that was already incomplete', () => {
1332+
const scopeMismatch = new ResolvedSecretTraceRegistry([], scope)
1333+
const foreignChild = new ResolvedSecretTraceRegistry([], {
1334+
userId: 'user-1',
1335+
workspaceId: 'workspace-2',
1336+
})
1337+
1338+
scopeMismatch.mergeToolCallRegistry(foreignChild)
1339+
1340+
expect(mockLogger.error).toHaveBeenCalledWith(
1341+
'Resolved secret registry marked incomplete',
1342+
expect.objectContaining({ reason: 'tool-call-scope-mismatch' })
1343+
)
1344+
1345+
mockLogger.warn.mockClear()
1346+
mockLogger.error.mockClear()
1347+
1348+
const sameScope = new ResolvedSecretTraceRegistry([], scope)
1349+
const incompleteChild = new ResolvedSecretTraceRegistry([], scope)
1350+
incompleteChild.markIncomplete('projection-mismatch')
1351+
mockLogger.error.mockClear()
1352+
1353+
sameScope.mergeToolCallRegistry(incompleteChild)
1354+
1355+
expect(mockLogger.error).not.toHaveBeenCalled()
1356+
expect(mockLogger.warn).toHaveBeenCalledWith(
1357+
'Resolved secret registry marked incomplete',
1358+
expect.objectContaining({ reason: 'inherited-incomplete-source' })
1359+
)
1360+
})
1361+
1362+
it('records no secret material alongside the reason', () => {
1363+
const registry = new ResolvedSecretTraceRegistry([], scope)
1364+
1365+
registry.recordResolved('MISSING', 'super-secret-value')
1366+
1367+
const logged = JSON.stringify(mockLogger.error.mock.calls)
1368+
expect(logged).not.toContain('super-secret-value')
1369+
expect(logged).not.toContain('MISSING')
1370+
})
1371+
})

0 commit comments

Comments
 (0)