Skip to content

feat(custom-blocks): join cross-workspace runs into the caller's trace, and map blocks per environment - #6857

Merged
icecrasher321 merged 4 commits into
stagingfrom
feat/custom-block-traces-and-fork-mapping
Aug 19, 2026
Merged

feat(custom-blocks): join cross-workspace runs into the caller's trace, and map blocks per environment#6857
icecrasher321 merged 4 commits into
stagingfrom
feat/custom-block-traces-and-fork-mapping

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

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 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 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: 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 already 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 (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

  • New feature

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 own lib/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 in workflow-handler.test.ts (nothing emitted without an authorized viewer; projection goes through the child session), and trace-spans.test.ts — which fails if someone widens isWorkflowBlockType and 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, the liveTraceViewerUserId gate in execution-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)

  • Permission groups gate allowedIntegrations by 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.
  • Hydration follows 3 boundary hops vs the executor's 25-deep call chain; deeper nests say "Not expanded (nesting limit)" rather than rendering as complete.
  • Parent trace detail depends on the child's log row surviving retention.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 19, 2026 8:50pm

Request Review

@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes cross-workspace authorization for traces and live streams and fork promote semantics for custom blocks—security-sensitive boundaries where mis-gating would leak publisher block internals or leave env forks invoking prod.

Overview
Custom blocks now expose what actually ran in the caller’s trace without baking another workspace’s internals into the parent log, and forked environments can remap block types so promote doesn’t silently keep calling the source env.

Trace / debugging. Parent spans store only the child run’s opaque childExecutionId; hydrateChildTraces joins child spans at read time after checkWorkspaceAccess on each source workspace (nested hops checked separately). The trace UI explains absent children via childTraceAccess (denied, missing, truncated). Live editor runs can stream child events only when liveTraceViewerUserId is set and the viewer can read the source workspace—via emit-only liveStreamCallbacks, not parent progress markers—and child spans are projected through the child logging session. childTraceSpans are stripped from persisted traces/outputs; block logs carry childExecution for correlation.

Fork / environments. Custom blocks become a mappable fork resource keyed by block type (remapForkBlockType, promote transformBlockType). Unmapped blocks keep the source type and block promote with unmapped-custom-block messaging (distinct from “would lose”). Mapping candidates are org-scoped with workspace labels; sourceWorkspaceName disambiguates same-named blocks in Access Control and the palette.

Execution UI. isSubExecutionBlockType treats custom blocks like workflow blocks in the terminal tree; canvas drill-down still collects snapshot ids after hydration.

Reviewed by Cursor Bugbot for commit 9065158. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR joins authorized cross-workspace custom-block executions into parent traces at read time and adds environment-specific custom-block mapping for workspace forks.

  • Propagates opaque child execution identifiers and hydrates child traces after per-workspace authorization.
  • Gates live child-span streaming on a known authenticated viewer and avoids persisting projected child spans into the parent trace.
  • Adds custom-block fork resources, block-type remapping, promotion blockers, and source-workspace labels.
  • Updates the database schema, migration metadata, API contracts, UI surfaces, and focused tests.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

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
Loading

Reviews (4): Last reviewed commit: "fix(custom-blocks): carry the emit-only ..." | Re-trigger Greptile

Comment thread apps/sim/ee/workspace-forking/lib/remap/remap-references.ts Outdated
icecrasher321 and others added 2 commits August 19, 2026 13:26
…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>
@icecrasher321
icecrasher321 force-pushed the feat/custom-block-traces-and-fork-mapping branch from e2c909d to 5b40970 Compare August 19, 2026 20:27
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/executor/execution/block-executor.ts
Comment thread apps/sim/executor/handlers/workflow/workflow-handler.ts
… 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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/executor/handlers/workflow/workflow-handler.ts
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@icecrasher321
icecrasher321 merged commit 9ee1b81 into staging Aug 19, 2026
30 checks passed
@icecrasher321
icecrasher321 deleted the feat/custom-block-traces-and-fork-mapping branch August 19, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant