Skip to content

Commit fbc6521

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 fbc6521

2 files changed

Lines changed: 262 additions & 28 deletions

File tree

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

Lines changed: 139 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,136 @@ 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('attributes an already-incomplete bundle to its source rather than to the value filter', async () => {
1363+
const registry = new ResolvedSecretTraceRegistry([], scope)
1364+
1365+
await registry.importProvenanceForValue(
1366+
{ version: 1, complete: false, entries: [], scope },
1367+
'x',
1368+
{
1369+
trusted: true,
1370+
inputPath: ['prompt'],
1371+
}
1372+
)
1373+
1374+
const reasons = mockLogger.error.mock.calls
1375+
.concat(mockLogger.warn.mock.calls)
1376+
.map(([, details]) => (details as { reason?: string })?.reason)
1377+
expect(reasons).toContain('source-provenance-incomplete')
1378+
expect(reasons).not.toContain('value-provenance-filter-incomplete')
1379+
})
1380+
1381+
it('reports an incoming incomplete bundle at error, since its origin logged in another process', () => {
1382+
const registry = new ResolvedSecretTraceRegistry([], scope)
1383+
1384+
registry.markIncomplete('source-provenance-incomplete')
1385+
1386+
expect(mockLogger.warn).not.toHaveBeenCalled()
1387+
expect(mockLogger.error).toHaveBeenCalledWith(
1388+
'Resolved secret registry marked incomplete',
1389+
expect.objectContaining({ reason: 'source-provenance-incomplete' })
1390+
)
1391+
})
1392+
1393+
it('reports an incoming incomplete bundle exactly once, from the registry that knows the path', async () => {
1394+
const registry = new ResolvedSecretTraceRegistry([], scope)
1395+
1396+
await registry.importProvenanceForValue(
1397+
{ version: 1, complete: false, entries: [], scope },
1398+
'x',
1399+
{ trusted: true, inputPath: ['prompt'] }
1400+
)
1401+
1402+
const records = mockLogger.error.mock.calls.concat(mockLogger.warn.mock.calls)
1403+
expect(records).toHaveLength(1)
1404+
expect(records[0]).toEqual([
1405+
'Resolved secret input path marked incomplete',
1406+
expect.objectContaining({ reason: 'source-provenance-incomplete', inputPath: 'prompt' }),
1407+
])
1408+
})
1409+
1410+
it('records no secret material alongside the reason', () => {
1411+
const registry = new ResolvedSecretTraceRegistry([], scope)
1412+
1413+
registry.recordResolved('MISSING', 'super-secret-value')
1414+
1415+
const logged = JSON.stringify(mockLogger.error.mock.calls)
1416+
expect(logged).not.toContain('super-secret-value')
1417+
expect(logged).not.toContain('MISSING')
1418+
})
1419+
})

0 commit comments

Comments
 (0)