Skip to content

Commit 5dbc342

Browse files
authored
fix(provenance): name the importer that condemned a run (#6493)
* fix(provenance): name the importer that condemned a run A bundle that arrives already incomplete latches the whole registry one-way, so every later model projection in the run refuses. #6483 made the refusal say what went wrong; it could not say who. In production the answer was reason=source-provenance-incomplete with 23 candidate importers and no way to tell them apart, which is where the last investigation stopped. Carry a stable origin across the import boundary and retain it beside the reason. It inherits through forks and merges — the step that erased attribution before, since a tool crossing forks, imports, then merges back — so the refusal names the importer even though the latch happened frames earlier. Tool crossings take the tool id, so a tool-sourced bundle identifies itself rather than being inferred from timestamps. Origins are caller-supplied strings rather than a closed union, so unlike reasons they carry an explicit bound. No behaviour change: the field is optional, additive, and read only when building a log record. * fix(provenance): attribute the path-scoped and short-circuit latches too importProvenanceForValueAtInputPath took only { trusted }, so the five callers that bind a crossing to an input path — the block, loop, parallel and workflow resolvers, and the guardrails route — could not name themselves. Separately, six latches sit beside a tagged import on the path where the import did not run or returned false: a bundle already marked incomplete short-circuits the || before the import, and each catch latches directly. Those reported no origin while their neighbour reported one. * fix(provenance): stop a broad catch claiming an incomplete bundle The catch around table-row provenance loading also covers a database failure in loadTableRowSecretProvenance, which is not an incomplete bundle. Naming a reason the catch cannot know is the misattribution this work exists to remove, so it reports 'unspecified' with its origin, matching every sibling catch. The decrypt catch keeps its specific reason because its try wraps only the decrypt call.
1 parent 156ee3e commit 5dbc342

31 files changed

Lines changed: 240 additions & 65 deletions

apps/sim/app/api/guardrails/validate/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ describe('POST /api/guardrails/validate', () => {
244244
expect(res.status).toBe(200)
245245
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, 'secret value', ['input'], {
246246
trusted: true,
247+
origin: 'guardrailsRoute.inputProvenance',
247248
})
248249
})
249250

apps/sim/app/api/guardrails/validate/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
267267
provenanceInspection.value,
268268
inputStr,
269269
['input'],
270-
{ trusted: true }
270+
{ trusted: true, origin: 'guardrailsRoute.inputProvenance' }
271271
)
272272
).success
273273
: true

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ async function projectWorkflowMcpModelContent(
288288
): Promise<unknown> {
289289
const registry = new ResolvedSecretTraceRegistry([], scope)
290290
const imported = await registry.importCrossingProvenance(privateProvenance, value, {
291+
origin: 'mcpServe.workflowCrossing',
291292
trusted: true,
292293
})
293294
if (!imported || !registry.isComplete()) {

apps/sim/app/api/providers/route.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,10 @@ describe('POST /api/providers', () => {
208208
)
209209

210210
expect(res.status).toBe(200)
211-
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, { trusted: true })
211+
expect(mockImportProvenance).toHaveBeenCalledWith(provenance, {
212+
trusted: true,
213+
origin: 'providersRoute.requestProvenance',
214+
})
212215
})
213216

214217
it('projects legacy private prompt provenance on the provider-facing copy', async () => {

apps/sim/app/api/providers/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
272272
const provenanceReady =
273273
await providerRuntimeContext.resolvedSecretTraceRegistry.importProvenance(
274274
provenanceInspection.value,
275-
{ trusted: true }
275+
{ trusted: true, origin: 'providersRoute.requestProvenance' }
276276
)
277277
if (!provenanceReady || !providerRuntimeContext.resolvedSecretTraceRegistry.isComplete()) {
278278
return NextResponse.json(

apps/sim/app/api/workflows/[id]/log/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ export const POST = withRouteHandler(
144144
if (trustedProvenance === undefined) {
145145
resolvedSecretTraceRegistry.markIncomplete()
146146
} else {
147-
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, { trusted: true })
147+
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, {
148+
trusted: true,
149+
origin: 'workflowLogRoute.trustedProvenance',
150+
})
148151
}
149152
loggingSession.setResolvedSecretTraceRegistry(resolvedSecretTraceRegistry)
150153

apps/sim/executor/handlers/mothership/mothership-handler.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ describe('MothershipBlockHandler', () => {
293293
content: 'raw secret remains functional',
294294
__resolvedSecretTraceProvenance: undefined,
295295
}),
296-
{ trusted: true }
296+
{ trusted: true, origin: 'mothership.payloadCrossing' }
297297
)
298298
expect(registry.markIncomplete).not.toHaveBeenCalled()
299299
expect(result).toMatchObject({ content: 'raw secret remains functional' })
@@ -519,7 +519,7 @@ describe('MothershipBlockHandler', () => {
519519
error: 'secret-backed failure',
520520
__resolvedSecretTraceProvenance: undefined,
521521
}),
522-
{ trusted: true }
522+
{ trusted: true, origin: 'mothership.payloadCrossing' }
523523
)
524524
expect(context.errorResolvedSecretTraceRegistry).toBeDefined()
525525
expect(context.errorResolvedSecretTraceRegistry).not.toBe(context.resolvedSecretTraceRegistry)
@@ -557,7 +557,7 @@ describe('MothershipBlockHandler', () => {
557557
error: 'secret-backed failure',
558558
__resolvedSecretTraceProvenance: undefined,
559559
}),
560-
{ trusted: true }
560+
{ trusted: true, origin: 'mothership.payloadCrossing' }
561561
)
562562
expect(registry.markIncomplete).not.toHaveBeenCalled()
563563
expect(context.errorResolvedSecretTraceRegistry).toBeDefined()
@@ -601,7 +601,7 @@ describe('MothershipBlockHandler', () => {
601601
content: 'unchanged',
602602
__resolvedSecretTraceProvenance: undefined,
603603
}),
604-
{ trusted: true }
604+
{ trusted: true, origin: 'mothership.payloadCrossing' }
605605
)
606606
expect(registry.markIncomplete).not.toHaveBeenCalled()
607607
expect(JSON.stringify(result.execution.output)).not.toContain('__resolvedSecretTraceProvenance')

apps/sim/executor/handlers/mothership/mothership-handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,10 @@ async function consumeMothershipProvenance(
378378

379379
if (!registry) return false
380380

381-
const imported = await registry.importProvenanceForValue(provenance, payload, { trusted: true })
381+
const imported = await registry.importProvenanceForValue(provenance, payload, {
382+
trusted: true,
383+
origin: 'mothership.payloadCrossing',
384+
})
382385
if (!imported) throw new Error('Mothership response provenance metadata is invalid')
383386
return true
384387
}

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ export class WorkflowBlockHandler implements BlockHandler {
450450
await childResolvedSecretTraceRegistry.importProvenance(crossingProvenance, {
451451
trusted: true,
452452
anonymous: true,
453+
origin: 'workflowHandler.childCrossing',
453454
})
454455
}
455456
// Custom-block children authenticate internal tool calls as the source
@@ -711,6 +712,7 @@ export class WorkflowBlockHandler implements BlockHandler {
711712
await ctx.resolvedSecretTraceRegistry.importProvenance(crossingProvenance, {
712713
trusted: true,
713714
anonymous: true,
715+
origin: 'workflowHandler.parentCrossing',
714716
})
715717
}
716718
return exposedOutput

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,35 @@ describe('refuseResolvedSecretProjection', () => {
195195
expect(refusalRecords()).toHaveLength(3)
196196
})
197197

198+
it('names the importer that condemned the run, through the fork and merge that hid it', async () => {
199+
const parent = new ResolvedSecretTraceRegistry([], scope)
200+
const fork = parent.forkForToolCall()
201+
await fork.importCrossingProvenance(
202+
{ version: 1, complete: false, entries: [], scope },
203+
{ rows: [] },
204+
{ trusted: true, origin: 'tool.table_query_rows' }
205+
)
206+
parent.mergeToolCallRegistry(fork)
207+
mockLogger.error.mockClear()
208+
mockLogger.warn.mockClear()
209+
210+
expect(() =>
211+
refuseResolvedSecretProjection({
212+
site: 'router.contextModelInput',
213+
message: 'Router model input could not be safely projected',
214+
registry: parent,
215+
inputPath: 'context,routes',
216+
})
217+
).toThrow()
218+
219+
expect(refusalRecords()[0][1]).toEqual(
220+
expect.objectContaining({
221+
reason: 'source-provenance-incomplete',
222+
origins: ['tool.table_query_rows'],
223+
})
224+
)
225+
})
226+
198227
it('records no secret material', () => {
199228
const registry = new ResolvedSecretTraceRegistry(
200229
[{ name: 'API_KEY', plaintext: 'super-secret-value', encryptedValue: 'encrypted' }],

0 commit comments

Comments
 (0)