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/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..c979642edfa 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 @@ -1167,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, @@ -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/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 a7aac65cd12..c2651bfa114 100644 --- a/apps/sim/lib/copilot/request/tools/client.ts +++ b/apps/sim/lib/copilot/request/tools/client.ts @@ -103,17 +103,20 @@ export async function waitForClientToolCompletion({ toolRegistry.markIncomplete() } else { const imported = await toolRegistry.importProvenance(sealedContext.provenance, { + origin: 'copilotToolClient.sealedContext', 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 da324d58906..d6f04a63c33 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -528,12 +528,19 @@ export async function resolveInputFiles( }) if ( !provenance.complete || - !(await resolvedSecretTraceRegistry.importProvenance(provenance, { trusted: true })) + !(await resolvedSecretTraceRegistry.importProvenance(provenance, { + trusted: true, + origin: 'copilotFunctionExecute.result', + })) ) { - resolvedSecretTraceRegistry.markIncomplete() + resolvedSecretTraceRegistry.markIncomplete('source-provenance-incomplete', { + origin: 'copilotFunctionExecute.result', + }) } } catch { - resolvedSecretTraceRegistry.markIncomplete() + resolvedSecretTraceRegistry.markIncomplete('unspecified', { + origin: 'copilotFunctionExecute.result', + }) } const columns = table.schema.columns @@ -561,11 +568,13 @@ 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() + if (!imported) + target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' }) } catch { - target.markIncomplete() + target.markIncomplete('unspecified', { origin: 'copilotFunctionExecute.crossing' }) } } 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..9fa5a7b05df 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,9 +304,12 @@ 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() + exactRegistry.markIncomplete('untrusted-provenance', { origin: 'traceStore.exactProvenance' }) } const [projected] = await projectTraceSpansForSecrets( 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