fix(logs): restore workflow input on log details for runs without secret provenance - #6278
fix(logs): restore workflow input on log details for runs without secret provenance#6278mzxchandra wants to merge 4 commits into
Conversation
…d module Moves extractLegacyWorkflowInput out of execution-state.ts so both the functional re-run reader and the log display projection can use it without a circular import (execution-state already imports trace-store). Adds two display-only helpers alongside it: hasPersistedBlockStates, and recoverLegacyWorkflowInputForDisplay, which narrows the raw trigger block output to the shape workflowInput originally held. The functional reader keeps first-match behavior; only the display path refuses to guess. No behavior change to re-execution.
…ret provenance PR #6000 added a resolved-secret gate to the log display projection: when a run's persisted resolvedSecretTraceProvenance is absent or incomplete, projectTraceSpansForSecrets returns structural-only spans, which stripped the whole display envelope. That silently blanked the Input and Output panels for every run written before #6000, and for any run whose registry goes incomplete. Splits the display envelope in two. Per-block content (finalOutput, blockInput, blockExecutions, errors, trace spans) keeps the existing gate and fails closed. Only workflowInput is exempted, via projectWorkflowBoundarySpansForSecrets: with complete provenance it is byte-identical to the current matcher path, and with absent or incomplete provenance the content survives instead of collapsing. workflowInput is exempt because an inbound trigger payload is captured before any secret is resolved. That premise does NOT hold for nested runs: a workflow or custom_block execution is handed workflowInput built from its parent's already-resolved block outputs, so those keep the gated treatment. The check is a denylist of nested trigger types, because webhook runs record the provider (zoho_desk, slack) as the trigger type and an allowlist would fail closed on exactly the population this fixes. Runs written before workflowInput was persisted recover it from the trigger block state, gated on an absent provenance key plus present block states, which together identify pre-stamping data without dating the row. Recovery refuses when more than one block state matches the trigger shape, since a paused run carries a resume placeholder with the same shape and its capability URL must never render as the workflow input. Verified against 137 executions on a live instance: the only display key that differs from the pre-fix baseline is workflowInput, on 72 runs, none lost.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryCursor Bugbot is generating a summary for commit a4cd453. Configure here. |
Greptile SummaryThe PR separates workflow-boundary input from downstream log content so legacy inbound input can render when secret provenance is unavailable, while retaining fail-closed projection for execution-derived content. It also extracts legacy input recovery and records source-execution metadata for inputs copied from prior runs.
Confidence Score: 4/5This PR is not yet safe to merge because execution-derived input can still be displayed without redaction on async/background and SSE runs. The reply states that the issue was fixed in 4717819, but the route stamps the source execution only on its own LoggingSession; async/background and SSE execution create separate sessions without that marker, so the display reader still classifies copied parent-resolved input as exempt boundary content. Files Needing Attention: apps/sim/app/api/workflows/[id]/execute/route.ts, apps/sim/background/workflow-execution.ts, apps/sim/lib/workflows/executor/execute-workflow.ts
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/workflows/[id]/execute/route.ts | Marks copied input on the route-owned logging session before dispatching execution. |
| apps/sim/lib/logs/execution/logging-session.ts | Adds source-execution metadata to trigger data when configured on a session. |
| apps/sim/lib/logs/execution/trace-store.ts | Splits boundary and gated display envelopes and uses trigger metadata to gate inherited input. |
| apps/sim/lib/logs/execution/trace-secret-projection.ts | Adds a boundary projection that preserves content when provenance is unavailable while retaining structural limits. |
| apps/sim/lib/logs/execution/legacy-workflow-input.ts | Extracts legacy input lookup and adds conservative display-specific recovery and shape normalization. |
Sequence Diagram
sequenceDiagram
participant API as Execute API
participant RouteSession as Route LoggingSession
participant Worker as Async/SSE executor
participant RunSession as Persisting LoggingSession
participant Store as Execution log
participant Display as Log projection
API->>RouteSession: setInputSourceExecutionId(source)
API->>Worker: pass resolved input
Worker->>RunSession: create fresh session
RunSession->>Store: persist trigger without source marker
Display->>Store: read workflowInput
Display->>Display: treat as boundary input
Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile
Greptile review: the boundary exemption assumed a non-nested trigger implies an inbound payload. A re-run with inputFromExecutionId breaks that. It copies the source run's workflowInput verbatim, and the destination presents whatever trigger type its caller asked for, so a value resolved inside a custom_block parent could arrive in a manual run and be exempted. The destination's own provenance cannot describe the source's secret resolution, so no matcher covers it. The read path could not tell an inherited input from a fresh one, so the re-run now records its source: LoggingSession.setInputSourceExecutionId stamps it into the existing trigger data channel, and the display projection withholds the exemption whenever it is present. Nested runs and inherited-input runs share one predicate - neither is a pre-resolution inbound payload. Adds setInputSourceExecutionId to the shared @sim/testing LoggingSession mock so the execute-route suites exercise the real call.
|
@greptile review |
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 983be88. Configure here.
| */ | ||
| if (inputFromExecutionId) { | ||
| loggingSession.setInputSourceExecutionId(inputFromExecutionId) | ||
| } |
There was a problem hiding this comment.
Async rerun omits input source
High Severity
For async runs with inputFromExecutionId, the setInputSourceExecutionId call on the route's LoggingSession isn't persisted. The worker creates a new session that lacks this ID, resulting in incomplete provenance. This can inadvertently expose copied secrets from the source execution in logs under the workflowInput boundary.
Reviewed by Cursor Bugbot for commit 983be88. Configure here.
|
fixed here centrally #6247 |


Summary
Workflow Input JSON renders empty on the logs page for a large class of executions. This restores it on the read path.
Root cause. PR #6000 added a resolved-secret gate to the log display projection. When a run's persisted
resolvedSecretTraceProvenanceis absent or incomplete,projectTraceSpansForSecretsreturns structural-only spans, which stripped the entire display envelope - includingworkflowInputandfinalOutput. Every run written before #6000 has no provenance key at all, so all of them went blank at once. The data was never lost; it is intact in storage and functional readers see it fine.Fix (
fix(logs): restore workflow input ...). The display envelope is split in two:finalOutput,blockInput,blockExecutions,error,errorDetails,completionFailure,message) keeps the existing fix(sanitization): secret exposure in function and agent trace spans #6000 gate and fails closed. Trace-span redaction is untouched.workflowInputonly) goes through a newprojectWorkflowBoundarySpansForSecrets. With complete provenance it is byte-identical to the current matcher path; with absent or incomplete provenance the content survives instead of collapsing to structure.workflowInputis exempt because an inbound trigger payload is captured before any secret is resolved. That premise does not hold for nested runs - aworkfloworcustom_blockexecution is handedworkflowInputassembled from its parent's already-resolved block outputs (workflow-handler.ts:578), so those keep the gated treatment. The check is a denylist of nested trigger types, because webhook runs record the provider (zoho_desk,slack) astrigger.typeand an allowlist of "known inbound" types would fail closed on exactly the population this fixes.Runs predating top-level
workflowInputpersistence recover it from the trigger block state, gated on an absent provenance key plus present block states - together these identify pre-stamping data without dating the row. Recovery refuses when more than one block state matches the trigger shape, because a paused run carries a resume placeholder with the sameexecuted:false / executionTime:0 / output != nullshape and its capability URL must never render as the workflow input.Refactor (
refactor(logs): extract the legacy workflow-input reader ...).extractLegacyWorkflowInputmoves out ofexecution-state.tsinto a shared module so the display path can use it without a circular import. The functional re-run reader keeps first-match behavior; only the display path refuses to guess.Deliberately not in scope:
finalOutputfails closed. It sits downstream of every block and can carry a resolved secret, and on the incomplete path there is no matcher to redact with.Test Coverage
Tests: 1487 files before → 1487 after (no new test files; 14 cases added to 3 existing files).
Pre-Landing Review
No issues found. Notable verifications rather than assumptions:
Promise.allover two projections sharing onestore.createProjectionContextbuilds a fresh context per call and all mutable state is per-context;storeis read-only config. No race.LOG_DISPLAY_CONTENT_KEYSwas split into two tuples - proved the union is the identical 8-key set, the constant is module-private with one consumer, and neither synthetic span id has an external consumer or leaks intodisplayData.traceSpans.Adversarial Review
Codex found three real issues that three of my own review passes missed. All fixed and re-verified (
GATE: PASS):Design Review
No frontend files changed - design review skipped. (
gstack-diff-scopereportsSCOPE_FRONTEND=true, but that is a false positive from the repo shape; all 6 changed files areapps/sim/lib/**/*.ts.)Eval Results
No prompt-related files changed - evals skipped.
Scope Drift
Scope Check: CLEAN. Intent was to restore workflow input/output on the logs read path; delivered exactly that in 4 source files (1 new) plus 3 test files. No write-path, schema, or API-contract changes.
Plan Completion
13 DONE, 3 CHANGED, 0 NOT DONE, 0 UNVERIFIABLE.
The 3 CHANGED items are all narrowings made during implementation, each with evidence:
finalOutputalongsideworkflowInput; it now fails closed instead.completeExecutionWithFinalization, which stamps, andonEnvironmentSnapshotfires unconditionally. Verified empirically (108 legacy blobs with the key absent, 5 current-code blobs with it present, no overlap) and pinned with alogging-sessiontest rather than a production change.Verification Results
Verified end to end against a live instance (Next.js + realtime + Postgres), using a second server running
stagingagainst the same database as an A/B baseline.workflowInput, 0 differ on any other key, 0 lost input.tokendropped and the value absent from the whole response; nested.inputnarrows with all 8 hoisted siblings gone; byte-identical shape renders verbatim.complete:falserun renders input while withholdingfinalOutput,blockInput, and span content.inputFromExecutionIdpointing at a legacy row with no top-level input; it went through the moved extractor and inherited the payload exactly.Known limitations
completeExecutionWithFinalizationacceptsworkflowInput, and thecompletedandpausedcallers pass it, butforce_failed,cancelled, and the fallback do not - so those runs persist no input at all and no read-path change can recover it. 42 such runs in the environment I tested. Deliberately left for a separate PR to keep this one read-path only.workflowInput: {}renders as an empty panel for runs that genuinely had no input (a manual run of a workflow whose Start block has no configured input fields). This matches pre-fix(sanitization): secret exposure in function and agent trace spans #6000 behavior and is intentional.Test plan
tsc --noEmitclean,biome checkcleancheck:api-validation,check:boundaries,check:client-boundary,check:utilsall passTwo tests fail only under full-suite parallel load and pass in isolation -
markdown-parse(a 400-seed property test with a 30s ceiling that takes ~10s idle and >45s loaded) anddiff-engine. Both are in files this PR does not touch and were failing onstagingbefore it.🤖 Generated with Claude Code
https://claude.ai/code/session_015BdMTqfpyGk33ExErtiPkZ