Skip to content

Commit cc61118

Browse files
improvement(provenance): attribute stored-envelope display reads to their execution (#7039)
* improvement(provenance): attribute stored-envelope display reads to their execution A display materialization of an execution log imports the row's stored provenance envelopes into throwaway registries, and each import of an incomplete envelope re-emitted the registry's own summary — per envelope, per view, carrying counts and a workspace but never the execution id. A reader repeatedly materializing the same stored rows produced hundreds of identical lines that could not say which executions to go look at, and the volume scaled with views of a state that was fully recorded when the run wrote it. Verified against production before changing anything: essentially no new incomplete envelopes are being stored since the writer fix shipped, and no data drains exist — the stream is bounded re-reads of old rows through the display paths, not a live producer. The display registries are now staged — the existing concept for a registry that filters one value for a caller that reports against the real boundary — and each display function reports once per materialization with the execution id, workflow, workspace, and the parts that could not be vouched for. Severity is preserved: an incomplete stored envelope stays at warn, a malformed one stays at error. Projection behavior is unchanged everywhere — incomplete and malformed envelopes still fail their values closed exactly as before; only the reporting moves to the boundary that knows the execution. * improvement(provenance): fold the incomplete-envelope predicate and pin dual-site reporting Review pass over the previous commit: one helper instead of three copies of the incomplete-envelope check, the staged TSDoc generalized to cover both of its uses, and the block-outputs entry point's two-site reporting of one run envelope documented and pinned rather than left implicit. * improvement(provenance): classify every unusable stored envelope at the display boundary Review findings from the first round, both accepted: a present-but- malformed block or run envelope was withheld with no attributed line, and a complete envelope whose entries fail decryption latched the staged registry with only the unattributed entry-level error. Fault classification moves into the one import helper the display paths share, which now returns the registry and the fault together: absent is not a fault, unparseable is malformed, unable-to-vouch is incomplete, and a complete envelope whose registry latched during import — entry decryption is the only latch on that trusted path — is undecryptable. Every consumer reports through the same table, severity per kind, so the exact-value loop stops being the only site that could name a malformed envelope. Withholding behavior is unchanged at every site.
1 parent 04380b7 commit cc61118

3 files changed

Lines changed: 362 additions & 31 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -880,9 +880,10 @@ export class ResolvedSecretTraceRegistry {
880880
private readonly scope?: ResolvedSecretTraceScopeV1
881881
private readonly completeProvenanceEnvelopeBytes: number
882882
/**
883-
* A staged registry filters one value and is then discarded. Its caller re-reports whatever
884-
* fault it hits against the real input path, so its own summary lines would restate that with
885-
* strictly less context. Entry-level detail still logs — the caller cannot reconstruct it.
883+
* A staged registry filters values for one operation and is then discarded. Its caller owns the
884+
* reporting and says it with strictly more context — the real input path for a value filter, the
885+
* execution for a display read — so the registry's own summary lines would only restate it.
886+
* Entry-level detail still logs — the caller cannot reconstruct it.
886887
*/
887888
private readonly staged: boolean
888889

apps/sim/lib/logs/execution/trace-store.test.ts

Lines changed: 240 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
*/
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
55

6-
const { decryptSecretMock, materializeLargeValueRefMock, storeLargeValueMock } = vi.hoisted(() => ({
7-
decryptSecretMock: vi.fn(),
8-
materializeLargeValueRefMock: vi.fn(),
9-
storeLargeValueMock: vi.fn(),
6+
const { decryptSecretMock, materializeLargeValueRefMock, storeLargeValueMock, mockLogger } =
7+
vi.hoisted(() => ({
8+
decryptSecretMock: vi.fn(),
9+
materializeLargeValueRefMock: vi.fn(),
10+
storeLargeValueMock: vi.fn(),
11+
mockLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
12+
}))
13+
14+
vi.mock('@sim/logger', () => ({
15+
createLogger: () => mockLogger,
1016
}))
1117

1218
vi.mock('@/lib/core/security/encryption', () => ({
@@ -536,3 +542,233 @@ describe('projectExecutionDataForDisplay provenance handling', () => {
536542
expect(displayData.traceSpans).toEqual([])
537543
})
538544
})
545+
546+
describe('stored provenance display reporting', () => {
547+
const REGISTRY_SUMMARY_MESSAGES = [
548+
'Resolved secret registry marked incomplete',
549+
'Resolved secret input path marked incomplete',
550+
]
551+
552+
function registrySummaryLines(): unknown[] {
553+
return [...mockLogger.warn.mock.calls, ...mockLogger.error.mock.calls].filter(([message]) =>
554+
REGISTRY_SUMMARY_MESSAGES.includes(message as string)
555+
)
556+
}
557+
558+
/**
559+
* The stored state was recorded when the run wrote it; a view re-deriving it must say which
560+
* execution it served, once — not restate the latch through registry summaries that name none.
561+
*/
562+
it('reports an incomplete stored envelope once, naming the execution and the parts', async () => {
563+
const displayData = await projectExecutionDataForDisplay(
564+
{
565+
finalOutput: { result: 'value' },
566+
executionState: {
567+
resolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
568+
finalOutputResolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
569+
},
570+
},
571+
CONTEXT
572+
)
573+
574+
expect(displayData).not.toHaveProperty('finalOutput')
575+
expect(registrySummaryLines()).toHaveLength(0)
576+
expect(mockLogger.warn).toHaveBeenCalledWith(
577+
'Stored execution provenance cannot vouch for display content',
578+
expect.objectContaining({
579+
site: 'traceStore.displayProjection',
580+
executionId: 'execution-1',
581+
workflowId: 'workflow-1',
582+
workspaceId: 'workspace-1',
583+
parts: ['traceSpans', 'finalOutput'],
584+
partCount: 2,
585+
})
586+
)
587+
expect(mockLogger.error).not.toHaveBeenCalled()
588+
})
589+
590+
it('reports a malformed stored envelope at error, keeping the value withheld', async () => {
591+
const displayData = await projectExecutionDataForDisplay(
592+
{
593+
finalOutput: { result: 'value' },
594+
executionState: {
595+
resolvedSecretTraceProvenance: {
596+
version: 1,
597+
complete: true,
598+
entries: [],
599+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
600+
},
601+
finalOutputResolvedSecretTraceProvenance: 'garbage',
602+
},
603+
},
604+
CONTEXT
605+
)
606+
607+
expect(displayData).not.toHaveProperty('finalOutput')
608+
expect(registrySummaryLines()).toHaveLength(0)
609+
expect(mockLogger.error).toHaveBeenCalledWith(
610+
'Stored execution provenance is malformed',
611+
expect.objectContaining({
612+
site: 'traceStore.displayProjection',
613+
executionId: 'execution-1',
614+
parts: ['finalOutput'],
615+
})
616+
)
617+
})
618+
619+
/** A complete envelope whose entries cannot be decrypted withholds content like any fault. */
620+
it('attributes an undecryptable stored envelope to its execution at error', async () => {
621+
decryptSecretMock.mockRejectedValue(new Error('key rotated'))
622+
623+
const displayData = await projectExecutionDataForDisplay(
624+
{
625+
finalOutput: { result: 'value' },
626+
executionState: {
627+
resolvedSecretTraceProvenance: {
628+
version: 1,
629+
complete: true,
630+
entries: [],
631+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
632+
},
633+
finalOutputResolvedSecretTraceProvenance: {
634+
version: 1,
635+
complete: true,
636+
entries: [{ name: 'SECRET', encryptedValue: 'ciphertext' }],
637+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
638+
},
639+
},
640+
},
641+
CONTEXT
642+
)
643+
644+
expect(displayData).not.toHaveProperty('finalOutput')
645+
expect(registrySummaryLines()).toHaveLength(0)
646+
expect(mockLogger.error).toHaveBeenCalledWith(
647+
'Stored execution provenance could not be decrypted',
648+
expect.objectContaining({
649+
site: 'traceStore.displayProjection',
650+
executionId: 'execution-1',
651+
parts: ['finalOutput'],
652+
})
653+
)
654+
})
655+
656+
it('reports a malformed block-output envelope at error, withholding the output', async () => {
657+
const result = await materializeExecutionDataForDisplayWithBlockOutputs(
658+
{
659+
executionState: {
660+
resolvedSecretTraceProvenance: {
661+
version: 1,
662+
complete: true,
663+
entries: [],
664+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
665+
},
666+
blockStates: {
667+
'block-1': { output: { value: 1 }, resolvedSecretTraceProvenance: 'garbage' },
668+
},
669+
},
670+
},
671+
CONTEXT,
672+
['block-1']
673+
)
674+
675+
expect(result.blockOutputs.has('block-1')).toBe(false)
676+
expect(registrySummaryLines()).toHaveLength(0)
677+
expect(mockLogger.error).toHaveBeenCalledWith(
678+
'Stored execution provenance is malformed',
679+
expect.objectContaining({
680+
site: 'traceStore.blockOutputs',
681+
executionId: 'execution-1',
682+
parts: ['blockOutput:block-1'],
683+
})
684+
)
685+
})
686+
687+
it('stays silent when every stored envelope is complete', async () => {
688+
const displayData = await projectExecutionDataForDisplay(
689+
{
690+
finalOutput: { result: 'direct-literal' },
691+
executionState: {
692+
resolvedSecretTraceProvenance: {
693+
version: 1,
694+
complete: true,
695+
entries: [],
696+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
697+
},
698+
finalOutputResolvedSecretTraceProvenance: {
699+
version: 1,
700+
complete: true,
701+
entries: [],
702+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
703+
},
704+
},
705+
},
706+
CONTEXT
707+
)
708+
709+
expect(displayData.finalOutput).toEqual({ result: 'direct-literal' })
710+
expect(mockLogger.warn).not.toHaveBeenCalled()
711+
expect(mockLogger.error).not.toHaveBeenCalled()
712+
})
713+
714+
/** The block entry point runs both display functions; each names its own site for the envelope. */
715+
it('attributes an incomplete run envelope under both sites on a block-outputs read', async () => {
716+
await materializeExecutionDataForDisplayWithBlockOutputs(
717+
{
718+
finalOutput: { result: 'value' },
719+
executionState: {
720+
resolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
721+
blockStates: {
722+
'block-1': { output: { value: 1 } },
723+
},
724+
},
725+
},
726+
CONTEXT,
727+
['block-1']
728+
)
729+
730+
expect(registrySummaryLines()).toHaveLength(0)
731+
expect(mockLogger.warn).toHaveBeenCalledWith(
732+
'Stored execution provenance cannot vouch for display content',
733+
expect.objectContaining({ site: 'traceStore.displayProjection', parts: ['traceSpans'] })
734+
)
735+
expect(mockLogger.warn).toHaveBeenCalledWith(
736+
'Stored execution provenance cannot vouch for display content',
737+
expect.objectContaining({ site: 'traceStore.blockOutputs', parts: ['run'] })
738+
)
739+
})
740+
741+
it('reports incomplete block-output envelopes once for the whole block read', async () => {
742+
const result = await materializeExecutionDataForDisplayWithBlockOutputs(
743+
{
744+
executionState: {
745+
resolvedSecretTraceProvenance: {
746+
version: 1,
747+
complete: true,
748+
entries: [],
749+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
750+
},
751+
blockStates: {
752+
'block-1': {
753+
output: { value: 1 },
754+
resolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
755+
},
756+
},
757+
},
758+
},
759+
CONTEXT,
760+
['block-1']
761+
)
762+
763+
expect(result.blockOutputs.has('block-1')).toBe(false)
764+
expect(registrySummaryLines()).toHaveLength(0)
765+
expect(mockLogger.warn).toHaveBeenCalledWith(
766+
'Stored execution provenance cannot vouch for display content',
767+
expect.objectContaining({
768+
site: 'traceStore.blockOutputs',
769+
executionId: 'execution-1',
770+
parts: ['blockOutput:block-1'],
771+
})
772+
)
773+
})
774+
})

0 commit comments

Comments
 (0)