From 11f734c202d49832e563b392e49fe4f88298d95b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 10 Aug 2026 09:47:20 -0700 Subject: [PATCH 1/3] fix(provenance): name the importer that condemned a run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../sim/app/api/mcp/serve/[serverId]/route.ts | 1 + apps/sim/app/api/providers/route.test.ts | 5 +- apps/sim/app/api/providers/route.ts | 2 +- apps/sim/app/api/workflows/[id]/log/route.ts | 5 +- .../mothership/mothership-handler.test.ts | 8 +- .../handlers/mothership/mothership-handler.ts | 5 +- .../handlers/workflow/workflow-handler.ts | 2 + ...resolved-secret-projection-refusal.test.ts | 29 ++++++ .../resolved-secret-projection-refusal.ts | 2 + .../resolved-secret-trace-registry.test.ts | 34 +++++++ .../utils/resolved-secret-trace-registry.ts | 89 ++++++++++++++----- apps/sim/lib/copilot/request/tools/client.ts | 1 + .../tools/handlers/function-execute.ts | 6 +- .../tools/handlers/workflow/mutations.ts | 4 +- .../execution/durable-secret-provenance.ts | 10 ++- .../lib/guardrails/validate_hallucination.ts | 1 + .../lib/knowledge/model-input-provenance.ts | 1 + .../sim/lib/logs/execution/logging-session.ts | 5 +- apps/sim/lib/logs/execution/trace-store.ts | 10 ++- apps/sim/lib/table/backfill-runner.ts | 5 +- apps/sim/lib/table/rows/secret-provenance.ts | 8 +- .../workspace-file-secret-provenance.test.ts | 10 +-- .../lib/workflows/executor/execution-core.ts | 2 +- .../executor/input-secret-provenance.ts | 5 +- apps/sim/tools/index.ts | 21 +++-- 25 files changed, 219 insertions(+), 52 deletions(-) diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index 211edf1305b..ee7cea60c34 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -288,6 +288,7 @@ async function projectWorkflowMcpModelContent( ): Promise { const registry = new ResolvedSecretTraceRegistry([], scope) const imported = await registry.importCrossingProvenance(privateProvenance, value, { + origin: 'mcpServe.workflowCrossing', trusted: true, }) if (!imported || !registry.isComplete()) { diff --git a/apps/sim/app/api/providers/route.test.ts b/apps/sim/app/api/providers/route.test.ts index 55641053694..e42cb93a08e 100644 --- a/apps/sim/app/api/providers/route.test.ts +++ b/apps/sim/app/api/providers/route.test.ts @@ -208,7 +208,10 @@ describe('POST /api/providers', () => { ) expect(res.status).toBe(200) - expect(mockImportProvenance).toHaveBeenCalledWith(provenance, { trusted: true }) + expect(mockImportProvenance).toHaveBeenCalledWith(provenance, { + trusted: true, + origin: 'providersRoute.requestProvenance', + }) }) it('projects legacy private prompt provenance on the provider-facing copy', async () => { diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index 2fe21daae23..7e6d9845381 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -272,7 +272,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const provenanceReady = await providerRuntimeContext.resolvedSecretTraceRegistry.importProvenance( provenanceInspection.value, - { trusted: true } + { trusted: true, origin: 'providersRoute.requestProvenance' } ) if (!provenanceReady || !providerRuntimeContext.resolvedSecretTraceRegistry.isComplete()) { return NextResponse.json( diff --git a/apps/sim/app/api/workflows/[id]/log/route.ts b/apps/sim/app/api/workflows/[id]/log/route.ts index 8e5123504ed..93984773999 100644 --- a/apps/sim/app/api/workflows/[id]/log/route.ts +++ b/apps/sim/app/api/workflows/[id]/log/route.ts @@ -144,7 +144,10 @@ export const POST = withRouteHandler( if (trustedProvenance === undefined) { resolvedSecretTraceRegistry.markIncomplete() } else { - await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, { trusted: true }) + await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, { + trusted: true, + origin: 'workflowLogRoute.trustedProvenance', + }) } loggingSession.setResolvedSecretTraceRegistry(resolvedSecretTraceRegistry) diff --git a/apps/sim/executor/handlers/mothership/mothership-handler.test.ts b/apps/sim/executor/handlers/mothership/mothership-handler.test.ts index 6c9a752ec05..aa211829f4f 100644 --- a/apps/sim/executor/handlers/mothership/mothership-handler.test.ts +++ b/apps/sim/executor/handlers/mothership/mothership-handler.test.ts @@ -293,7 +293,7 @@ describe('MothershipBlockHandler', () => { content: 'raw secret remains functional', __resolvedSecretTraceProvenance: undefined, }), - { trusted: true } + { trusted: true, origin: 'mothership.payloadCrossing' } ) expect(registry.markIncomplete).not.toHaveBeenCalled() expect(result).toMatchObject({ content: 'raw secret remains functional' }) @@ -519,7 +519,7 @@ describe('MothershipBlockHandler', () => { error: 'secret-backed failure', __resolvedSecretTraceProvenance: undefined, }), - { trusted: true } + { trusted: true, origin: 'mothership.payloadCrossing' } ) expect(context.errorResolvedSecretTraceRegistry).toBeDefined() expect(context.errorResolvedSecretTraceRegistry).not.toBe(context.resolvedSecretTraceRegistry) @@ -557,7 +557,7 @@ describe('MothershipBlockHandler', () => { error: 'secret-backed failure', __resolvedSecretTraceProvenance: undefined, }), - { trusted: true } + { trusted: true, origin: 'mothership.payloadCrossing' } ) expect(registry.markIncomplete).not.toHaveBeenCalled() expect(context.errorResolvedSecretTraceRegistry).toBeDefined() @@ -601,7 +601,7 @@ describe('MothershipBlockHandler', () => { content: 'unchanged', __resolvedSecretTraceProvenance: undefined, }), - { trusted: true } + { trusted: true, origin: 'mothership.payloadCrossing' } ) expect(registry.markIncomplete).not.toHaveBeenCalled() expect(JSON.stringify(result.execution.output)).not.toContain('__resolvedSecretTraceProvenance') diff --git a/apps/sim/executor/handlers/mothership/mothership-handler.ts b/apps/sim/executor/handlers/mothership/mothership-handler.ts index b3af3d828d5..1d285cd5df3 100644 --- a/apps/sim/executor/handlers/mothership/mothership-handler.ts +++ b/apps/sim/executor/handlers/mothership/mothership-handler.ts @@ -378,7 +378,10 @@ async function consumeMothershipProvenance( if (!registry) return false - const imported = await registry.importProvenanceForValue(provenance, payload, { trusted: true }) + const imported = await registry.importProvenanceForValue(provenance, payload, { + trusted: true, + origin: 'mothership.payloadCrossing', + }) if (!imported) throw new Error('Mothership response provenance metadata is invalid') return true } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index e795c2b99e9..73aa5d4fbf7 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -450,6 +450,7 @@ export class WorkflowBlockHandler implements BlockHandler { await childResolvedSecretTraceRegistry.importProvenance(crossingProvenance, { trusted: true, anonymous: true, + origin: 'workflowHandler.childCrossing', }) } // Custom-block children authenticate internal tool calls as the source @@ -711,6 +712,7 @@ export class WorkflowBlockHandler implements BlockHandler { await ctx.resolvedSecretTraceRegistry.importProvenance(crossingProvenance, { trusted: true, anonymous: true, + origin: 'workflowHandler.parentCrossing', }) } return exposedOutput diff --git a/apps/sim/executor/utils/resolved-secret-projection-refusal.test.ts b/apps/sim/executor/utils/resolved-secret-projection-refusal.test.ts index 17cdfef8d63..16924a314dd 100644 --- a/apps/sim/executor/utils/resolved-secret-projection-refusal.test.ts +++ b/apps/sim/executor/utils/resolved-secret-projection-refusal.test.ts @@ -195,6 +195,35 @@ describe('refuseResolvedSecretProjection', () => { expect(refusalRecords()).toHaveLength(3) }) + it('names the importer that condemned the run, through the fork and merge that hid it', async () => { + const parent = new ResolvedSecretTraceRegistry([], scope) + const fork = parent.forkForToolCall() + await fork.importCrossingProvenance( + { version: 1, complete: false, entries: [], scope }, + { rows: [] }, + { trusted: true, origin: 'tool.table_query_rows' } + ) + parent.mergeToolCallRegistry(fork) + mockLogger.error.mockClear() + mockLogger.warn.mockClear() + + expect(() => + refuseResolvedSecretProjection({ + site: 'router.contextModelInput', + message: 'Router model input could not be safely projected', + registry: parent, + inputPath: 'context,routes', + }) + ).toThrow() + + expect(refusalRecords()[0][1]).toEqual( + expect.objectContaining({ + reason: 'source-provenance-incomplete', + origins: ['tool.table_query_rows'], + }) + ) + }) + it('records no secret material', () => { const registry = new ResolvedSecretTraceRegistry( [{ name: 'API_KEY', plaintext: 'super-secret-value', encryptedValue: 'encrypted' }], diff --git a/apps/sim/executor/utils/resolved-secret-projection-refusal.ts b/apps/sim/executor/utils/resolved-secret-projection-refusal.ts index bd8837d7245..24a573225ea 100644 --- a/apps/sim/executor/utils/resolved-secret-projection-refusal.ts +++ b/apps/sim/executor/utils/resolved-secret-projection-refusal.ts @@ -64,6 +64,8 @@ function reportRefusal({ site, registry, inputPath }: ResolvedSecretProjectionRe ? { reason: diagnostics.reasons[0], reasons: diagnostics.reasons, + /** The importers that cost the run its completeness — who to go and look at. */ + ...(diagnostics.origins.length > 0 ? { origins: diagnostics.origins } : {}), incompleteInputPathCount: diagnostics.incompleteInputPathCount, activeEntryCount: diagnostics.activeEntryCount, ...(diagnostics.scopeWorkspaceId diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts index 6c2f057e2fb..e16030c2d88 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts @@ -1562,6 +1562,40 @@ describe('incompleteness diagnostics', () => { expect(registry.getIncompletenessDiagnostics()?.reasons[0]).toBe('constructed-incomplete') }) + it('attributes an untrustworthy bundle to the caller that imported it', async () => { + const registry = new ResolvedSecretTraceRegistry([], scope) + + await registry.importProvenance( + { version: 1, complete: false, entries: [], scope }, + { trusted: true, origin: 'workflowHandler.childCrossing' } + ) + + expect(registry.getIncompletenessDiagnostics()?.origins).toEqual([ + 'workflowHandler.childCrossing', + ]) + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Resolved secret registry marked incomplete', + expect.objectContaining({ + reason: 'source-provenance-incomplete', + origin: 'workflowHandler.childCrossing', + }) + ) + }) + + it('bounds retained origins, which are caller-supplied rather than a closed union', async () => { + const registry = new ResolvedSecretTraceRegistry([], scope) + + for (let index = 0; index < 20; index++) { + await registry.importProvenance( + { version: 1, complete: false, entries: [], scope }, + { trusted: true, origin: `caller.${index}` } + ) + } + + expect(registry.getIncompletenessDiagnostics()?.origins).toHaveLength(8) + expect(registry.getIncompletenessDiagnostics()?.origins[0]).toBe('caller.0') + }) + it('records no secret material alongside the reason', () => { const registry = new ResolvedSecretTraceRegistry([], scope) diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.ts index 377c89d72da..0b772c603ef 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.ts @@ -79,6 +79,12 @@ const BY_DESIGN_INCOMPLETENESS_REASONS = new Set } +/** Extra attribution for a latch: which registry it propagated from, and which importer caused it. */ +interface MarkIncompleteContext { + source?: ResolvedSecretTraceRegistry + origin?: string +} + export interface ImportResolvedSecretTraceProvenanceOptions { trusted: boolean anonymous?: boolean + /** + * Stable dotted identifier for the caller, e.g. `workflowHandler.childCrossing`. + * + * A bundle that arrives already incomplete condemns the whole run, and the reason alone cannot + * say which of the many importers accepted it. Recording the caller is what turns + * `source-provenance-incomplete` from a symptom into an address. + */ + origin?: string } export interface ExportResolvedSecretTraceProvenanceForValueOptions { @@ -627,6 +649,8 @@ export class ResolvedSecretTraceRegistry { private readonly incompleteInputPaths = new Map() /** Insertion-ordered; see {@link ResolvedSecretIncompletenessDiagnostics}. */ private readonly incompletenessReasons = new Set() + /** Import callers that cost this registry its completeness; bounded by {@link MAX_RETAINED_ORIGINS}. */ + private readonly incompletenessOrigins = new Set() private activeProvenanceEntryBytes = 0 private complete = true private pendingActivations = 0 @@ -681,7 +705,7 @@ export class ResolvedSecretTraceRegistry { } this.copyResolvedInputPathsTo(fork) this.copyIncompleteInputPathsTo(fork) - if (!this.complete) fork.markIncomplete('inherited-incomplete-source', this) + if (!this.complete) fork.markIncomplete('inherited-incomplete-source', { source: this }) return fork } @@ -692,12 +716,12 @@ export class ResolvedSecretTraceRegistry { ): ResolvedSecretTraceRegistry { const fork = new ResolvedSecretTraceRegistry(this.catalog.values(), this.scope) if (!this.complete) { - fork.markIncomplete('inherited-incomplete-source', this) + fork.markIncomplete('inherited-incomplete-source', { source: this }) return fork } if (this.hasIncompleteInputPathOverlapping(paths)) { - fork.markIncomplete('inherited-incomplete-input-path', this) + fork.markIncomplete('inherited-incomplete-input-path', { source: this }) return fork } @@ -721,7 +745,8 @@ export class ResolvedSecretTraceRegistry { fork.addActiveEntry({ ...entry }, { propagated: true }) } } - if (this.isPermanentlyIncomplete()) fork.markIncomplete('inherited-incomplete-source', this) + if (this.isPermanentlyIncomplete()) + fork.markIncomplete('inherited-incomplete-source', { source: this }) return fork } @@ -733,7 +758,7 @@ export class ResolvedSecretTraceRegistry { } if (!child.isComplete()) { - this.markIncomplete('inherited-incomplete-source', child) + this.markIncomplete('inherited-incomplete-source', { source: child }) return } @@ -1100,12 +1125,12 @@ export class ResolvedSecretTraceRegistry { options: ImportResolvedSecretTraceProvenanceOptions ): Promise { if (!options.trusted || !isResolvedSecretTraceProvenanceV1(provenance)) { - this.markIncomplete('untrusted-provenance') + this.markIncomplete('untrusted-provenance', { origin: options.origin }) return false } if (!provenance.complete) { - this.markIncomplete('source-provenance-incomplete') + this.markIncomplete('source-provenance-incomplete', { origin: options.origin }) } const sameScope = scopesMatch(provenance.scope, this.scope) @@ -1128,7 +1153,7 @@ export class ResolvedSecretTraceRegistry { importedAll = false decryptFailures += 1 firstDecryptError ??= getErrorMessage(error, 'Unknown error') - this.markIncomplete('entry-decrypt-failed') + this.markIncomplete('entry-decrypt-failed', { origin: options.origin }) } } @@ -1156,7 +1181,7 @@ export class ResolvedSecretTraceRegistry { async importProvenanceForValue( provenance: unknown, value: unknown, - options: { trusted: boolean } + options: { trusted: boolean; origin?: string } ): Promise { const result = await this.importProvenanceForValueInternal(provenance, value, options) return result.success @@ -1178,28 +1203,39 @@ export class ResolvedSecretTraceRegistry { private async importProvenanceForValueInternal( provenance: unknown, value: unknown, - options: { trusted: boolean; inputPath?: ResolvedSecretInputPath } + options: { trusted: boolean; inputPath?: ResolvedSecretInputPath; origin?: string } ): Promise { if (!options.trusted || !isResolvedSecretTraceProvenanceV1(provenance)) { - this.markInputPathIncomplete(options.inputPath, 'value-provenance-untrusted') + this.markInputPathIncomplete(options.inputPath, 'value-provenance-untrusted', options.origin) return { success: false, matched: false } } const sourceRegistry = new ResolvedSecretTraceRegistry([], provenance.scope, { staged: true }) - const sourceImported = await sourceRegistry.importProvenance(provenance, { trusted: true }) + const sourceImported = await sourceRegistry.importProvenance(provenance, { + trusted: true, + origin: options.origin, + }) const filteredProvenance = sourceRegistry.exportProvenanceForValue(value) if (!sourceImported) { - this.markInputPathIncomplete(options.inputPath, 'value-provenance-import-failed') + this.markInputPathIncomplete( + options.inputPath, + 'value-provenance-import-failed', + options.origin + ) return { success: false, matched: false } } if (!filteredProvenance.complete) { this.markInputPathIncomplete( options.inputPath, - provenance.complete ? 'value-provenance-filter-incomplete' : 'source-provenance-incomplete' + provenance.complete ? 'value-provenance-filter-incomplete' : 'source-provenance-incomplete', + options.origin ) return { success: true, matched: false } } - const filteredImported = await this.importProvenance(filteredProvenance, { trusted: true }) + const filteredImported = await this.importProvenance(filteredProvenance, { + trusted: true, + origin: options.origin, + }) if (options.inputPath && options.inputPath.length > 0 && filteredProvenance.complete) { const sameScope = scopesMatch(filteredProvenance.scope, this.scope) this.bindResolvedInputPathEntries( @@ -1228,7 +1264,7 @@ export class ResolvedSecretTraceRegistry { async importCrossingProvenance( provenance: unknown, crossingValue: unknown, - options: { trusted: boolean } + options: { trusted: boolean; origin?: string } ): Promise { return this.importProvenanceForValue(provenance, crossingValue, options) } @@ -1351,6 +1387,7 @@ export class ResolvedSecretTraceRegistry { if (!this.isPermanentlyIncomplete()) return undefined return { reasons: [...this.incompletenessReasons], + origins: [...this.incompletenessOrigins], incompleteInputPathCount: this.incompleteInputPaths.size, activeEntryCount: this.activeEntries.size, ...(this.scope?.workspaceId ? { scopeWorkspaceId: this.scope.workspaceId } : {}), @@ -1362,12 +1399,19 @@ export class ResolvedSecretTraceRegistry { this.incompletenessReasons.add(reason) } + /** Retains the importing caller, keeping the earliest once the bound is reached. */ + private recordIncompletenessOrigin(origin: string): void { + if (this.incompletenessOrigins.size >= MAX_RETAINED_ORIGINS) return + this.incompletenessOrigins.add(origin) + } + /** * Carries a source registry's reasons into a fork or merge target, so a refusal downstream still * names the guard that originally tripped rather than only the propagation that reached it. */ private inheritIncompletenessReasonsFrom(source: ResolvedSecretTraceRegistry): void { for (const reason of source.incompletenessReasons) this.recordIncompletenessReason(reason) + for (const origin of source.incompletenessOrigins) this.recordIncompletenessOrigin(origin) } isPermanentlyIncomplete(): boolean { @@ -1376,16 +1420,18 @@ export class ResolvedSecretTraceRegistry { markIncomplete( reason: ResolvedSecretIncompletenessReason = 'unspecified', - source?: ResolvedSecretTraceRegistry + context: MarkIncompleteContext = {} ): void { - if (source) this.inheritIncompletenessReasonsFrom(source) + if (context.source) this.inheritIncompletenessReasonsFrom(context.source) this.recordIncompletenessReason(reason) + if (context.origin) this.recordIncompletenessOrigin(context.origin) if (!this.complete) return this.complete = false this.modelEgressRevision += 1 if (this.staged || BY_DESIGN_INCOMPLETENESS_REASONS.has(reason)) return const details = { reason, + ...(context.origin ? { origin: context.origin } : {}), scopeWorkspaceId: this.scope?.workspaceId, activeEntryCount: this.activeEntries.size, incompleteInputPathCount: this.incompleteInputPaths.size, @@ -1824,13 +1870,15 @@ export class ResolvedSecretTraceRegistry { private markInputPathIncomplete( path: ResolvedSecretInputPath | undefined, - reason: ResolvedSecretIncompletenessReason = 'unspecified' + reason: ResolvedSecretIncompletenessReason = 'unspecified', + origin?: string ): void { if (!path || path.length === 0) { - this.markIncomplete(reason) + this.markIncomplete(reason, { origin }) return } this.recordIncompletenessReason(reason) + if (origin) this.recordIncompletenessOrigin(origin) const key = inputPathKey(path) if (this.incompleteInputPaths.has(key)) return this.incompleteInputPaths.set(key, [...path]) @@ -1838,6 +1886,7 @@ export class ResolvedSecretTraceRegistry { if (this.staged || BY_DESIGN_INCOMPLETENESS_REASONS.has(reason)) return const details = { reason, + ...(origin ? { origin } : {}), inputPath: path.join('.'), scopeWorkspaceId: this.scope?.workspaceId, activeEntryCount: this.activeEntries.size, diff --git a/apps/sim/lib/copilot/request/tools/client.ts b/apps/sim/lib/copilot/request/tools/client.ts index a7aac65cd12..3ab6d5b0142 100644 --- a/apps/sim/lib/copilot/request/tools/client.ts +++ b/apps/sim/lib/copilot/request/tools/client.ts @@ -103,6 +103,7 @@ export async function waitForClientToolCompletion({ toolRegistry.markIncomplete() } else { const imported = await toolRegistry.importProvenance(sealedContext.provenance, { + origin: 'copilotToolClient.sealedContext', trusted: true, }) if (!imported || !sealedContext.provenance.complete) { diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index da324d58906..6a840040710 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -528,7 +528,10 @@ export async function resolveInputFiles( }) if ( !provenance.complete || - !(await resolvedSecretTraceRegistry.importProvenance(provenance, { trusted: true })) + !(await resolvedSecretTraceRegistry.importProvenance(provenance, { + trusted: true, + origin: 'copilotFunctionExecute.result', + })) ) { resolvedSecretTraceRegistry.markIncomplete() } @@ -561,6 +564,7 @@ async function importMountedProvenance( try { const provenance = source.exportProvenanceForValue(crossingValue) const imported = await target.importCrossingProvenance(provenance, crossingValue, { + origin: 'copilotFunctionExecute.crossing', trusted: true, }) if (!imported) target.markIncomplete() diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 260bf73fbd2..dc74ef922ae 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -119,7 +119,7 @@ async function executeCopilotWorkflowTarget(params: { await params.context.resolvedSecretTraceRegistry.importCrossingProvenance( result.executionState?.resolvedSecretTraceProvenance, { output: result.output, logs: result.logs, error: result.error }, - { trusted: true } + { trusted: true, origin: 'copilotWorkflowMutation.runCrossing' } ) } return result @@ -134,7 +134,7 @@ async function executeCopilotWorkflowTarget(params: { error: executionResult?.error, thrownMessage: toError(error).message, }, - { trusted: true } + { trusted: true, origin: 'copilotWorkflowMutation.failedRunCrossing' } ) } if (admission.targetReservation) { diff --git a/apps/sim/lib/execution/durable-secret-provenance.ts b/apps/sim/lib/execution/durable-secret-provenance.ts index edd4a1b7df8..724f0ac340d 100644 --- a/apps/sim/lib/execution/durable-secret-provenance.ts +++ b/apps/sim/lib/execution/durable-secret-provenance.ts @@ -256,8 +256,14 @@ export async function importDurableSecretProvenance( } const imported = value === undefined - ? await registry.importProvenance(envelope, { trusted: true }) - : await registry.importProvenanceForValue(envelope, value, { trusted: true }) + ? await registry.importProvenance(envelope, { + trusted: true, + origin: 'durableProvenance.envelope', + }) + : await registry.importProvenanceForValue(envelope, value, { + trusted: true, + origin: 'durableProvenance.valueEnvelope', + }) complete = imported && complete } return complete && !registry.isPermanentlyIncomplete() diff --git a/apps/sim/lib/guardrails/validate_hallucination.ts b/apps/sim/lib/guardrails/validate_hallucination.ts index 7185236baaf..155af832993 100644 --- a/apps/sim/lib/guardrails/validate_hallucination.ts +++ b/apps/sim/lib/guardrails/validate_hallucination.ts @@ -143,6 +143,7 @@ async function queryKnowledgeBase( functionalResponse = { ...payload } delete functionalResponse[RESOLVED_SECRET_PROVENANCE_FIELD] const imported = await resultRegistry.importProvenance(inspection.value, { + origin: 'guardrails.hallucinationResult', trusted: true, }) if (!imported || !resultRegistry.isComplete()) { diff --git a/apps/sim/lib/knowledge/model-input-provenance.ts b/apps/sim/lib/knowledge/model-input-provenance.ts index af4ea906476..30fbfe8f706 100644 --- a/apps/sim/lib/knowledge/model-input-provenance.ts +++ b/apps/sim/lib/knowledge/model-input-provenance.ts @@ -65,6 +65,7 @@ export async function prepareKnowledgeModelInputProvenance(options: { } const imported = await registry.importProvenanceForValue(inspection.value, options.modelInput, { + origin: 'knowledge.modelInputProvenance', trusted: true, }) if (!imported || !registry.isComplete()) { diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index d6b51552096..ced3101d10c 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -316,7 +316,10 @@ export class LoggingSession { } const registry = new ResolvedSecretTraceRegistry([], provenance.scope) - await registry.importProvenance(provenance, { trusted: true }) + await registry.importProvenance(provenance, { + trusted: true, + origin: 'loggingSession.restoredProvenance', + }) return registry } diff --git a/apps/sim/lib/logs/execution/trace-store.ts b/apps/sim/lib/logs/execution/trace-store.ts index 031d86216d0..f251c1ce903 100644 --- a/apps/sim/lib/logs/execution/trace-store.ts +++ b/apps/sim/lib/logs/execution/trace-store.ts @@ -275,7 +275,10 @@ export async function projectExecutionDataForDisplay( if (isResolvedSecretTraceProvenanceV1(provenance)) { registry = new ResolvedSecretTraceRegistry([], provenance.scope) - await registry.importProvenance(provenance, { trusted: true }) + await registry.importProvenance(provenance, { + trusted: true, + origin: 'traceStore.spanProvenance', + }) } const projectionStore = { @@ -301,7 +304,10 @@ export async function projectExecutionDataForDisplay( ? new ResolvedSecretTraceRegistry([], exactProvenance.scope) : new ResolvedSecretTraceRegistry() if (isResolvedSecretTraceProvenanceV1(exactProvenance)) { - await exactRegistry.importProvenance(exactProvenance, { trusted: true }) + await exactRegistry.importProvenance(exactProvenance, { + trusted: true, + origin: 'traceStore.exactProvenance', + }) } else { exactRegistry.markIncomplete() } diff --git a/apps/sim/lib/table/backfill-runner.ts b/apps/sim/lib/table/backfill-runner.ts index c9041f84b87..1d549397ed5 100644 --- a/apps/sim/lib/table/backfill-runner.ts +++ b/apps/sim/lib/table/backfill-runner.ts @@ -82,7 +82,10 @@ export async function createBackfillExecutionSecretRegistry(options: { registry.markIncomplete() return registry } - await registry.importProvenance(provenance, { trusted: true }) + await registry.importProvenance(provenance, { + trusted: true, + origin: 'tableBackfill.rowProvenance', + }) return registry } diff --git a/apps/sim/lib/table/rows/secret-provenance.ts b/apps/sim/lib/table/rows/secret-provenance.ts index 160bffa73c1..7ea7291107f 100644 --- a/apps/sim/lib/table/rows/secret-provenance.ts +++ b/apps/sim/lib/table/rows/secret-provenance.ts @@ -256,7 +256,13 @@ export async function createTableRowSecretProvenanceFromEncryptedExecution( return createUnknownTableRowSecretProvenance() } const registry = new ResolvedSecretTraceRegistry([], provenance.scope) - if (!(await registry.importProvenance(provenance, { trusted: true })) || !registry.isComplete()) { + if ( + !(await registry.importProvenance(provenance, { + trusted: true, + origin: 'tableRow.encryptedExecution', + })) || + !registry.isComplete() + ) { return createUnknownTableRowSecretProvenance() } return createTableRowSecretProvenanceFromRegistry(data, registry) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts index a565b08510b..124c66e0337 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts @@ -450,7 +450,7 @@ describe('workspace file secret provenance', () => { ], scope: { userId: 'user-1' }, }, - { trusted: true } + { trusted: true, origin: 'durableProvenance.envelope' } ) queueTableRows(workspaceFiles, [ @@ -488,7 +488,7 @@ describe('workspace file secret provenance', () => { ], scope: { userId: 'user-1' }, }, - { trusted: true } + { trusted: true, origin: 'durableProvenance.envelope' } ) queueTableRows(workspaceFiles, [ @@ -523,7 +523,7 @@ describe('workspace file secret provenance', () => { entries: [{ encryptedValue: 'anonymous-encrypted' }], scope: { userId: 'user-1' }, }, - { trusted: true } + { trusted: true, origin: 'durableProvenance.envelope' } ) queueTableRows(workspaceFiles, [ @@ -583,7 +583,7 @@ describe('workspace file secret provenance', () => { ], scope: { userId: 'user-1' }, }, - { trusted: true } + { trusted: true, origin: 'durableProvenance.envelope' } ) }) @@ -669,7 +669,7 @@ describe('workspace file secret provenance', () => { scope: { userId: 'user-1' }, }, 'derived text', - { trusted: true } + { trusted: true, origin: 'durableProvenance.valueEnvelope' } ) }) diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 90af6185620..aa1fd9094f5 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -544,7 +544,7 @@ async function executeWorkflowCoreImpl( if (options.trustedInitialResolvedSecretTraceProvenance !== undefined) { await resolvedSecretTraceRegistry.importProvenance( options.trustedInitialResolvedSecretTraceProvenance, - { trusted: true } + { trusted: true, origin: 'executionCore.initialProvenance' } ) } loggingSession.setResolvedSecretTraceRegistry(resolvedSecretTraceRegistry) diff --git a/apps/sim/lib/workflows/executor/input-secret-provenance.ts b/apps/sim/lib/workflows/executor/input-secret-provenance.ts index bc93c6f4ec4..ecfbc83e1ed 100644 --- a/apps/sim/lib/workflows/executor/input-secret-provenance.ts +++ b/apps/sim/lib/workflows/executor/input-secret-provenance.ts @@ -85,7 +85,10 @@ export async function resolveWorkflowInputSecretProvenance(options: { } const sourceRegistry = new ResolvedSecretTraceRegistry([], provenance.scope) - const imported = await sourceRegistry.importProvenance(provenance, { trusted: true }) + const imported = await sourceRegistry.importProvenance(provenance, { + trusted: true, + origin: 'executionInput.secretProvenance', + }) const inputProvenance = sourceRegistry.exportProvenanceForValue(options.input) if ( !imported || diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 22981884f81..6e36e73d284 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -1187,7 +1187,8 @@ function consumeResolvedSecretNames( async function consumeResolvedSecretProvenance( payload: unknown, - registry?: ResolvedSecretTraceRegistry + registry: ResolvedSecretTraceRegistry | undefined, + toolId: string ): Promise { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false @@ -1202,6 +1203,7 @@ async function consumeResolvedSecretProvenance( const targetRegistry = registry.forkForToolCall() const imported = await targetRegistry.importCrossingProvenance(provenance, response, { trusted: true, + origin: `tool.${toolId}`, }) if (!imported) return false registry.mergeToolCallRegistry(targetRegistry) @@ -1249,7 +1251,8 @@ async function consumePrivateToolPayloadMetadata( headers: Headers, requestedType: PrivateToolMetadataType | undefined, params: Record, - registry?: ResolvedSecretTraceRegistry + registry: ResolvedSecretTraceRegistry | undefined, + toolId: string ): Promise { if (!requestedType) return 'verified' @@ -1308,7 +1311,7 @@ async function consumePrivateToolPayloadMetadata( ) { if (!consumeResolvedSecretNames(record, params, registry)) return 'invalid' } else { - if (!(await consumeResolvedSecretProvenance(record, registry))) return 'invalid' + if (!(await consumeResolvedSecretProvenance(record, registry, toolId))) return 'invalid' } } catch { return 'invalid' @@ -1323,7 +1326,8 @@ async function consumePrivateToolResponseMetadata( response: Response, requestedType: PrivateToolMetadataType | undefined, params: Record, - registry?: ResolvedSecretTraceRegistry + registry: ResolvedSecretTraceRegistry | undefined, + toolId: string ): Promise { if (!requestedType) return { response } @@ -1347,7 +1351,8 @@ async function consumePrivateToolResponseMetadata( response.headers, requestedType, params, - registry + registry, + toolId ) if (consumption === 'invalid') { return { response: rebuildSafePrivateToolResponse(response) } @@ -2512,7 +2517,8 @@ async function executeToolRequest( response, privateToolMetadataType, params, - resolvedSecretTraceRegistry + resolvedSecretTraceRegistry, + toolId ) response = privateMetadata.response @@ -2938,7 +2944,8 @@ async function executeMcpTool( response, privateToolMetadataType, params, - resolvedSecretTraceRegistry + resolvedSecretTraceRegistry, + toolId ) ).response From fa9536b520a51abad0bf2a07bcc63239dae4f316 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 10 Aug 2026 10:01:00 -0700 Subject: [PATCH 2/3] fix(provenance): attribute the path-scoped and short-circuit latches too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/sim/app/api/guardrails/validate/route.test.ts | 1 + apps/sim/app/api/guardrails/validate/route.ts | 2 +- .../utils/resolved-secret-trace-registry.ts | 2 +- apps/sim/executor/variables/resolvers/block.ts | 2 +- apps/sim/executor/variables/resolvers/loop.ts | 2 +- apps/sim/executor/variables/resolvers/parallel.ts | 2 +- apps/sim/executor/variables/resolvers/workflow.ts | 2 +- apps/sim/lib/copilot/request/tools/client.ts | 6 ++++-- .../lib/copilot/tools/handlers/function-execute.ts | 13 +++++++++---- apps/sim/lib/logs/execution/trace-store.ts | 2 +- 10 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/api/guardrails/validate/route.test.ts b/apps/sim/app/api/guardrails/validate/route.test.ts index 5ae3dbe996a..797e7852af0 100644 --- a/apps/sim/app/api/guardrails/validate/route.test.ts +++ b/apps/sim/app/api/guardrails/validate/route.test.ts @@ -244,6 +244,7 @@ describe('POST /api/guardrails/validate', () => { expect(res.status).toBe(200) expect(mockImportProvenance).toHaveBeenCalledWith(provenance, 'secret value', ['input'], { trusted: true, + origin: 'guardrailsRoute.inputProvenance', }) }) diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index f0e50dcfa9d..d6edea5b4ef 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -267,7 +267,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { provenanceInspection.value, inputStr, ['input'], - { trusted: true } + { trusted: true, origin: 'guardrailsRoute.inputProvenance' } ) ).success : true diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.ts index 0b772c603ef..c979642edfa 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.ts @@ -1192,7 +1192,7 @@ export class ResolvedSecretTraceRegistry { provenance: unknown, value: unknown, inputPath: ResolvedSecretInputPath | undefined, - options: { trusted: boolean } + options: { trusted: boolean; origin?: string } ): Promise { return this.importProvenanceForValueInternal(provenance, value, { ...options, diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index b057a97501c..83f20eecd27 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -337,7 +337,7 @@ export class BlockResolver implements Resolver { state.resolvedSecretTraceProvenance, value, context.inputPath, - { trusted: true } + { trusted: true, origin: 'blockResolver.outputCrossing' } ) if (imported.matched) context.onResolvedSecretReference?.() return value diff --git a/apps/sim/executor/variables/resolvers/loop.ts b/apps/sim/executor/variables/resolvers/loop.ts index 3c29af6a45c..523af3cfc9f 100644 --- a/apps/sim/executor/variables/resolvers/loop.ts +++ b/apps/sim/executor/variables/resolvers/loop.ts @@ -283,7 +283,7 @@ export class LoopResolver implements Resolver { provenance, resolvedValue, context.inputPath, - { trusted: true } + { trusted: true, origin: 'loopResolver.itemCrossing' } ) if (imported.matched) context.onResolvedSecretReference?.() return resolvedValue diff --git a/apps/sim/executor/variables/resolvers/parallel.ts b/apps/sim/executor/variables/resolvers/parallel.ts index fdec9d5cdd0..12c95cf9ca8 100644 --- a/apps/sim/executor/variables/resolvers/parallel.ts +++ b/apps/sim/executor/variables/resolvers/parallel.ts @@ -390,7 +390,7 @@ export class ParallelResolver implements Resolver { provenance, resolvedValue, context.inputPath, - { trusted: true } + { trusted: true, origin: 'parallelResolver.itemCrossing' } ) if (imported.matched) context.onResolvedSecretReference?.() return resolvedValue diff --git a/apps/sim/executor/variables/resolvers/workflow.ts b/apps/sim/executor/variables/resolvers/workflow.ts index 7bc0a6299ad..8d0c778016b 100644 --- a/apps/sim/executor/variables/resolvers/workflow.ts +++ b/apps/sim/executor/variables/resolvers/workflow.ts @@ -133,7 +133,7 @@ export class WorkflowResolver implements Resolver { provenance, value, context.inputPath, - { trusted: true } + { trusted: true, origin: 'workflowResolver.inputCrossing' } ) if (imported.matched) context.onResolvedSecretReference?.() return value diff --git a/apps/sim/lib/copilot/request/tools/client.ts b/apps/sim/lib/copilot/request/tools/client.ts index 3ab6d5b0142..c2651bfa114 100644 --- a/apps/sim/lib/copilot/request/tools/client.ts +++ b/apps/sim/lib/copilot/request/tools/client.ts @@ -107,14 +107,16 @@ export async function waitForClientToolCompletion({ trusted: true, }) if (!imported || !sealedContext.provenance.complete) { - toolRegistry.markIncomplete() + toolRegistry.markIncomplete('source-provenance-incomplete', { + origin: 'copilotToolClient.sealedContext', + }) } else { content = sealedContent } } } } catch { - toolRegistry?.markIncomplete() + toolRegistry?.markIncomplete('unspecified', { origin: 'copilotToolClient.sealedContext' }) } finally { finishPendingActivation?.() } diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 6a840040710..5e538f49362 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -533,10 +533,14 @@ export async function resolveInputFiles( origin: 'copilotFunctionExecute.result', })) ) { - resolvedSecretTraceRegistry.markIncomplete() + resolvedSecretTraceRegistry.markIncomplete('source-provenance-incomplete', { + origin: 'copilotFunctionExecute.result', + }) } } catch { - resolvedSecretTraceRegistry.markIncomplete() + resolvedSecretTraceRegistry.markIncomplete('source-provenance-incomplete', { + origin: 'copilotFunctionExecute.result', + }) } const columns = table.schema.columns @@ -567,9 +571,10 @@ async function importMountedProvenance( origin: 'copilotFunctionExecute.crossing', trusted: true, }) - if (!imported) target.markIncomplete() + if (!imported) + target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' }) } catch { - target.markIncomplete() + target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' }) } } diff --git a/apps/sim/lib/logs/execution/trace-store.ts b/apps/sim/lib/logs/execution/trace-store.ts index f251c1ce903..9fa5a7b05df 100644 --- a/apps/sim/lib/logs/execution/trace-store.ts +++ b/apps/sim/lib/logs/execution/trace-store.ts @@ -309,7 +309,7 @@ export async function projectExecutionDataForDisplay( origin: 'traceStore.exactProvenance', }) } else { - exactRegistry.markIncomplete() + exactRegistry.markIncomplete('untrusted-provenance', { origin: 'traceStore.exactProvenance' }) } const [projected] = await projectTraceSpansForSecrets( From 3ef04022c4ccbe66befeba7b2c509abce6644c41 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 10 Aug 2026 10:10:55 -0700 Subject: [PATCH 3/3] 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. --- apps/sim/lib/copilot/tools/handlers/function-execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 5e538f49362..d6f04a63c33 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -538,7 +538,7 @@ export async function resolveInputFiles( }) } } catch { - resolvedSecretTraceRegistry.markIncomplete('source-provenance-incomplete', { + resolvedSecretTraceRegistry.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.result', }) }