feat(custom-blocks): join cross-workspace runs into the caller's trace, and map blocks per environment - #6857
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Trace / debugging. Parent spans store only the child run’s opaque Fork / environments. Custom blocks become a mappable fork resource keyed by block type ( Execution UI. Reviewed by Cursor Bugbot for commit 9065158. Configure here. |
Greptile SummaryThe PR joins authorized cross-workspace custom-block executions into parent traces at read time and adds environment-specific custom-block mapping for workspace forks.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/logs/execution/hydrate-child-traces.ts | Adds bounded recursive child-trace hydration with per-workspace viewer authorization, cycle handling, and explicit denied, missing, or truncated states. |
| apps/sim/executor/handlers/workflow/workflow-handler.ts | Propagates authorized live-viewer context across custom-block execution boundaries while projecting child spans through the child logging session. |
| apps/sim/lib/workflows/executor/execution-core.ts | Adds the authenticated client-session viewer identity and emit-only callbacks used to gate live cross-workspace traces. |
| apps/sim/ee/workspace-forking/lib/remap/remap-references.ts | Treats custom-block types as fork-remappable references and preserves unmapped source types for promotion blocking. |
| apps/sim/ee/workspace-forking/lib/promote/sync-blockers.ts | Adds an explicit promotion blocker for unresolved custom-block mappings. |
| apps/sim/ee/workspace-forking/lib/mapping/resources.ts | Adds organization-scoped custom-block mapping candidates keyed by the block type referenced by workflows. |
| packages/db/migrations/0293_custom_block_fork_resource.sql | Extends the workspace-fork resource enum with the custom-block resource type. |
| apps/sim/app/api/logs/execution/[executionId]/route.ts | Hydrates authorized child traces before collecting child workflow snapshots for execution details. |
Sequence Diagram
sequenceDiagram
participant Viewer
participant Parent as Parent workspace
participant Hydrator as Trace hydrator
participant Authz as Workspace authorization
participant Child as Child workspace
Viewer->>Parent: Read parent execution trace
Parent->>Hydrator: Hydrate boundary execution IDs
Hydrator->>Child: Resolve child execution and workspace
Hydrator->>Authz: Check viewer access to child workspace
alt Access granted
Authz-->>Hydrator: Granted
Hydrator->>Child: Load and project child spans
Child-->>Hydrator: Authorized spans
Hydrator-->>Parent: Attach child spans
else Access denied or data unavailable
Authz-->>Hydrator: Denied
Hydrator-->>Parent: Preserve opaque boundary status
end
Parent-->>Viewer: Authorized composite trace
Reviews (4): Last reviewed commit: "fix(custom-blocks): carry the emit-only ..." | Re-trigger Greptile
…e, and map blocks per environment Teams that orchestrate work across workspaces have two gaps that keep them on HTTP blocks instead of custom blocks: they cannot see what a custom block actually did, and a forked environment silently keeps calling the environment it was forked from. Debugging. A custom block is an invocation boundary — a published block is org-wide, so its internals must not reach every consumer by default. The child already writes its own log row in the source workspace, correlated to the invoking run; the trace existed, it just was not joined. The parent's span now carries only the child's opaque execution id, and `hydrateChildTraces` joins the child's spans at READ time, after authorizing the person reading against the child's workspace. Authorization follows the viewer rather than a flag set at publish time, re-evaluates on every read, and needs no second copy of the spans. Each hop of a nested chain is authorized against its own workspace. Boundaries left unexpanded — no access, no data, past a cap — say so, because a childless boundary span otherwise renders exactly like a leaf and a partial trace reads as a complete one. Live runs stream too, gated on `liveTraceViewerUserId`, which only surfaces with a single known authenticated viewer set. Chat deployments stream through the same callbacks and their consumer may be anonymous, so anything that does not opt in keeps the boundary shut. Child spans handed to such a viewer are projected through the CHILD's session: the invoking run's registry knows nothing about the publisher's secrets, so projecting there would leave a source-owner credential unmasked. They reach the live stream and stop — `createSpanFromLog` still refuses to persist them, which is what keeps read-time hydration the single authorization point. Environments. A fork inherits its parent's organization and `custom_block` is keyed `(organization_id, type)`, so a uat fork resolved to the same row and ran the prod workflow. Custom blocks become a fork-mappable resource, keyed by BLOCK TYPE — the rule every kind follows: key by whatever the workflow references, as `file` does with storage keys and `env-var` with names. A custom block is the only resource referenced by the canvas block's own type rather than a sub-block value, so the rewrite gets its own channel. Unmapped blocks keep the source type, because a type cannot be emptied without deleting the node; they surface as unmapped and block the promote, which is what stops uat from quietly invoking prod. Same-named environment copies now carry their source workspace, so an Access Control allowlist decision between three identical "Invoice Parser" rows is no longer a guess. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… resolve `remapForkBlockType` reported a mapping whose target equalled the source as unresolved, conflating "a mapping exists" with "the type string changed". Those are opposite states that produce an identical output `type`, and every caller uses the flag for the former — to decide whether the reference blocks a promote. The org-wide candidate list includes the source block, so binding an environment to the shared block is a normal pick. Under the old flag it raised `unmapped-custom-block` and refused the sync over a choice the user had explicitly made. The flag is now named `resolved` and reports mapping existence; whether the type moved is already visible from `type`. Reported by Cursor Bugbot on #6857. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e2c909d to
5b40970
Compare
|
@cursor review |
… parent's log Two leaks in the live-stream path, both the same mistake: treating a channel as viewer-scoped when it is actually persisted, so gating the stream on an authorized viewer bought nothing. `childTraceSpans` rode the block output to reach the stream. `filterOutputForLog` only dropped a hidden key when the block's own config declared it `hiddenFromDisplay` — true of the workflow block, never of a custom block, whose outputs are publisher-curated. The source run's spans therefore persisted into the parent's `span.output`, readable by anyone with parent-workspace access and never re-checked by `hydrateChildTraces`. A globally hidden key is now dropped at the top level, not only when nested, and `extractDisplayOutput` strips it again so no other producer can reintroduce it. The fan-out also called the invoking run's `onBlockStart`/`onBlockComplete`, which are persist-then-emit composites: they write block names and I/O into the parent's LoggingSession before reaching the stream. Those markers are keyed by the parent execution and outlive the per-viewer check entirely. Custom-block children now go through `liveStreamCallbacks`, the raw emit-only pair, and fail closed when a surface supplies none. Same-workspace workflow children keep the composites — they belong to the same run and their markers are legitimately the parent's. Reported by Cursor Bugbot on #6857. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cursor review |
…tions
Routing custom-block events through `liveStreamCallbacks` forwarded the viewer id
to the child but not the sink itself, so a nested hop cleared
`canStreamCustomBlockToViewer` off the inherited id and then had nothing to stream
through — `parentStreamSink` fell back to `{}` and live traces stopped at the
first sub-executor. That hit a custom block nested inside a workflow block as
readily as one inside another custom block.
The sink now travels with the viewer id, and both are withheld together when
streaming is not permitted. It is always the INHERITED chain, never
`parentStreamSink`: for a same-workspace workflow block that is the persisting
composite, so forwarding it would put a custom block nested inside one straight
back onto the parent's progress markers — the leak the previous commit closed.
Reported by Cursor Bugbot on #6857.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9065158. Configure here.
Summary
Teams orchestrating work across workspaces have two gaps that keep them on HTTP blocks instead of custom blocks: they can't see what a custom block actually did, and a forked environment silently keeps calling the environment it was forked from. This closes both.
Debugging. A custom block is an invocation boundary — a published block is org-wide, so its internals must not reach every consumer by default. The child already writes its own log row in the source workspace, correlated to the invoking run; the trace existed, it just wasn't joined. The parent's span now carries only the child's opaque execution id, and
hydrateChildTracesjoins the child's spans at read time, after authorizing the person reading against the child's workspace. Authorization follows the viewer rather than a flag set at publish time, re-evaluates on every read, and needs no second copy of the spans. Each hop of a nested chain is authorized against its own workspace. Boundaries left unexpanded — no access, no data, past a cap — say so, because a childless boundary span otherwise renders exactly like a leaf and a partial trace reads as a complete one.Live runs stream too, gated on
liveTraceViewerUserId, which is only set by surfaces with a single known authenticated viewer. Chat deployments stream through the same callbacks and their consumer may be anonymous, so anything that doesn't opt in keeps the boundary shut. Child spans handed to such a viewer are projected through the child's session — the invoking run's registry knows nothing about the publisher's secrets, so projecting there would leave a source-owner credential unmasked. They reach the live stream and stop:createSpanFromLogstill refuses to persist them, which is what keeps read-time hydration the single authorization point.Environments. A fork inherits its parent's organization and
custom_blockis keyed(organization_id, type), so a uat fork resolved to the same row and ran the prod workflow. Custom blocks become a fork-mappable resource keyed by block type — the rule every kind already follows: key by whatever the workflow references, asfiledoes with storage keys andenv-varwith names. A custom block is the only resource referenced by the canvas block's owntyperather than a sub-block value, so the rewrite gets its own channel. Unmapped blocks keep the source type (a type can't be emptied without deleting the node) and instead surface as unmapped and block the promote — that's what stops uat from quietly invoking prod.Same-named environment copies now carry their source workspace, so an Access Control allowlist decision between three identical "Invoice Parser" rows is no longer a guess.
Type of Change
Testing
bun run test(full suite),turbo run type-check(24/24),check:migrations,check:api-validation, biome — all clean on this branch rebased onto latest staging. 3482 tests across the affected surfaces.Two full-suite failures are pre-existing and were verified as such rather than assumed:
lib/knowledge/documents/unreadable-document.test.ts— fails against staging's ownlib/knowledge/documents/(checked out and re-run); staging's OCR rewrite broke it without updating the test.lib/auth/sso-trust.test.ts— a 10s-timeout test sensitive to transform-cache temperature. On a clean tree: cold cache → fails, warm cache → passes. Unrelated to this branch.New coverage worth reviewing:
hydrate-child-traces.test.ts(denied / missing / truncated / nested-per-workspace authorization, cost stripping, cycle + cap handling), the boundary tests inworkflow-handler.test.ts(nothing emitted without an authorized viewer; projection goes through the child session), andtrace-spans.test.ts— which fails if someone widensisWorkflowBlockTypeand starts persisting child spans into the parent trace.Reviewers should focus on: the schema-quality of the read-time authorization in
hydrate-child-traces.ts, theliveTraceViewerUserIdgate inexecution-core.ts(it is the only thing standing between a chat visitor and another workspace's block names), and the unmapped-custom-block promote blocker.Known follow-ups (deliberately out of scope)
allowedIntegrationsby block type, so a remapped fork block falls out of its groups until an admin allowlists the target type. Auto-carrying group membership across a mapping would be a privilege escalation performed on the admin's behalf, so it needs a product call. The workspace suffix added here makes the manual fix legible.Checklist
🤖 Generated with Claude Code