Skip to content

v0.7.23: comparison table, security hardening, subflow & streaming improvements #5421

Merged
waleedlatif1 merged 16 commits into
mainfrom
staging
Jul 5, 2026
Merged

v0.7.23: comparison table, security hardening, subflow & streaming improvements #5421
waleedlatif1 merged 16 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 11 commits July 3, 2026 20:58
…roll (#5409)

* fix(compare): swap LangChain logo, fix comparison-table horizontal scroll

Replace LangChainIcon's path with the official brand mark (light-blue
link icon) instead of the prior monochrome recreation.

The comparison table's grid cells were missing min-w-0, so a grid item
without it sizes to its content's max-content width instead of
respecting its column's fr track, pushing the whole table wider than
its container and forcing a horizontal scrollbar. Add min-w-0 to every
grid cell/header, and size the row-label column to minmax(140px,
max-content) instead of a guessed fr ratio, so it's exactly as wide as
its longest label ("Vetted first-party integrations") needs and no
wider, leaving the Sim/competitor value columns their full share.

* fix(compare): stacked mobile layout for the comparison table

Below sm (640px) a 3-column table has no room to be legible even with
the sticky label column, so switch to the standard responsive-table
pattern instead: each fact stacks as label -> Sim's value -> the
competitor's value, each value tagged with its product name since the
column headers are no longer directly above. Pure CSS (max-sm:/sm:
variants), no JS, keeping this a zero-hydration server component.

* fix(compare): fix SourceLink inline-anchor overflow, align table breakpoint

Root cause of the persistent table-overflow/mobile-scroll issues: SourceLink
renders a plain <a> with no explicit display, so it defaults to
'display: inline'. min-width and truncate are no-ops on inline elements, so
any fact with a source (nearly all of them) ignored its flex/grid parent's
width constraint and could force the whole row (and thus the whole table)
wider than intended, regardless of any container-level min-w-0/max-content
fix. Give the anchor 'block min-w-0' so width constraints and truncation
actually cascade down to the wrapped value.

Also aligns the table's mobile-stack breakpoint from an ad hoc sm (640px) to
lg (1024px), matching this route group's own tablet-and-below convention
(.claude/rules for the (landing) group), and fixes the stacked mobile cells
to override the cell's base items-center with items-stretch so the name tag
and value get a real full-width box to truncate within instead of shrinking
to their own content size with no boundary.

* fix(compare): add min-w-0 to ColumnHeader per Greptile review

ColumnHeader (the Sim/competitor logo header cells in the two fr columns)
still had default min-width: auto, so an unusually long competitor name
could size the header to its min-content width and push the grid wider
than its column allows, same root cause as the rows already fixed.
…med messages (#5411)

* fix(mothership): stop chat perf decay from permanently-animated streamed messages

* fix(mothership): reset animation latches when a reused ChatContent gets replaced content

* fix(mothership): keep streaming parser on settled messages to kill the drain-swap flash

* fix(mothership): unify plain/special render branches to stop whole-message re-fade when options arrive

* improvement(mothership): hold render-phase animation latches in useState per updated hook rules

* fix(styling): translucent text-selection in form controls so field text stays readable

* improvement(styling): one lighter translucent text-selection color everywhere, no forced text color

* chore(styling): selection-muted tokens as 8-digit hex to match color token convention
…5407)

* feat(custom-block): deploy a workflow as a reusable org-scoped block

* fix(custom-block): reseed deploy form, guard duplicate publish, run child deployed

* test(custom-block): isolate custom-block rows fetch in execution-core test

* fix(custom-block): allow cross-workspace exec, org-scope authority, keep field ids, hide disabled

* feat(custom-block): run child under source owner's identity, workspace, and env

* fix(custom-block): bind publish authz to the source workflow's workspace

* fix(custom-block): gate edit/delete on source-workspace admin, not org admin

* chore(custom-block): rebaseline route count to 887 after staging merge

* fix(custom-block): sanitize failure output so it can't leak source workflow internals

* fix(custom-block): derive inputs and curated outputs from deployed state, not draft

* fix(custom-block): hide disabled blocks from the toolbar palette too

* fix(custom-block): bill nested + failed-run hosted cost; expose real inputs to the agent

* fix(custom-block): enforce enterprise + flag gate at every consumption path
…5416)

* fix(copilot): validate credential-link URL scheme before rendering

Only render the credential connect link as a clickable anchor when its
value resolves to an http(s) URL, reusing the isSafeHttpUrl helper
already used for chat file links.

* refactor(copilot): move isSafeHttpUrl to shared lib/core/utils/urls

Per Greptile's convention feedback: isSafeHttpUrl was consumed by both
chat and workspace/home but defined inside a feature-specific 'use client'
component. Move it alongside getBrowserOrigin (which it already depends
on) in lib/core/utils/urls.ts, matching this repo's shared-utility rule.
)

Google Drive, Slack, and OneDrive download routes fetched provider file
content without a response size cap, unlike the SharePoint download route.
Add maxResponseBytes to each content fetch, reject Google Drive files whose
metadata size already exceeds the cap before starting the download, and map
the resulting size-limit error to a clean 413 response.
…ge-document upload routes (#5413)

* fix(uploads): bound multipart body read in workspace-file and knowledge-document upload routes

* fix(uploads): avoid FormData-body stream race in bounded-read tests

* fix(uploads): share MAX_MULTIPART_OVERHEAD_BYTES constant across upload routes
* fix(stt): bound audio download response size

Cap the audioUrl download in the STT proxy route at the platform's standard 100MB file-size ceiling, matching the pattern already used by sharepoint/download-file and other external download routes. Classify size-limit rejections as a clean 413 instead of an unhandled 500.

* fix(stt): avoid double isPayloadSizeLimitError call in error handling
… requests (#5415)

* fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound requests

Route Email Bison webhook create/delete requests through the shared
DNS-validated, IP-pinned fetch used by the Teams and Slack webhook
providers instead of a raw fetch to the user-configured instance URL.

* fix(webhooks): restore NEXT_PUBLIC_APP_URL in emailbison tests, dedupe strict-delete warning log

Test env var mutation was never restored, risking cross-file leakage in
single-threaded vitest runs. Strict-mode deleteSubscription failures were
logged twice (once at the throw site with context, once generically by the
outer catch); the outer catch now skips its own log for errors already
logged at the throw site.
…p, dedupe permission-gate tests (#5420)

/simplify pass on PR #5404: resolve the execution-context workspace permission
check once per request instead of once per uploaded file, and collapse
repeated request-construction boilerplate in the new permission-gate tests
into a shared helper.
…h() calls (#5419)

PR #5399 fixed the callback route forgetting to pass fetchFn into the SDK's
auth(), but every call site (start + callback routes) still imported the raw
SDK auth() directly and had to remember to pass fetchFn: createSsrfGuardedMcpFetch()
by hand — the same omission was possible again at any future call site.

Add mcpAuthGuarded() in lib/mcp/oauth/auth.ts, a thin wrapper around the SDK's
auth() that always defaults fetchFn to the SSRF-guarded fetch (still overridable
for tests). Both routes now import mcpAuthGuarded from @/lib/mcp/oauth instead
of the raw SDK auth, so omitting the guard is no longer possible by omission.
@vercel

vercel Bot commented Jul 4, 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 Jul 5, 2026 2:18am

Request Review

@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Custom blocks introduce a new execution and API surface with org/workspace admin rules; concurrent security changes touch MCP OAuth, tool proxies, and multipart parsers where misconfiguration could block legitimate uploads or leave SSRF gaps.

Overview
Deploy as block (enterprise) adds org-scoped custom blocks: list/publish/update/delete APIs behind deploy-as-block + enterprise plan, a Block tab in the deploy modal (icon, curated outputs from deployed state), client/server registry overlays so custom_block_* types resolve on canvas, toolbar, access control, and execution via workflow_executor + withCustomBlockOverlay.

Security and abuse limits centralize isSafeHttpUrl in @/lib/core/utils/urls (chat downloads, credential connect links); MCP OAuth routes use mcpAuthGuarded instead of passing raw SDK auth(); Google Drive / OneDrive / Slack tool downloads and STT audio fetches enforce maxResponseBytes with 413 on overflow; workspace, knowledge, and v1 upload paths use readFormDataWithLimit; execution file uploads run the workspace permission check once per request outside the per-file loop.

Landing comparison table gains lg+ sticky row labels, a stacked mobile layout with per-column name tags, and truncation fixes on source links.

Mothership chat reduces streaming cost: word-paced fade, a one-way drain that remounts Streamdown to drop per-char spans after reveal, unified plain/special-tag rendering to avoid end-of-message re-fade, plus a fade cutoff on very long segments; global ::selection switches to --selection-muted so mirrored inputs stay readable.

Embedded workflow canvas disables drag-stop re-parenting into loops/parallels while still allowing repositioning.

Reviewed by Cursor Bugbot for commit f456ed9. Bugbot is set up for automated code reviews on this repo. Configure here.

const { data: customBlocksData } = useCustomBlocks(workspaceId)
useEffect(() => {
for (const cb of customBlocksData ?? []) blockConfigCache.current.delete(cb.type)
}, [customBlocksData])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale cache after unpublish

Medium Severity

When a custom block is unpublished, its type disappears from useCustomBlocks, but the canvas cache only clears types still in that list. getBlockConfig can keep serving the removed block’s cached config, so nodes may show outdated names, icons, and client-side schema until a full page refresh.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4b13398. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This release bundles multiple security hardening improvements (SSRF validation on EmailBison webhook calls, bounded response sizes for Drive/OneDrive/Slack/STT downloads, multipart body limits on file uploads, MCP OAuth SSRF guard enforcement) alongside the deploy-as-block feature that lets workspace admins publish a deployed workflow as a reusable org-scoped custom block, and two performance/UX fixes (streaming tree drain to eliminate DOM-node accumulation in long chat sessions, subflow re-parenting guard in embedded workflow views).

  • Security hardening: Five download/upload routes now enforce MAX_FILE_SIZE/MAX_MULTIPART_OVERHEAD_BYTES before buffering; EmailBison webhook calls validate and pin apiBaseUrl via DNS; MCP OAuth exchanges are now routed through a dedicated mcpAuthGuarded wrapper that defaults to the SSRF-guarded fetch.
  • Deploy-as-block feature: New custom_block DB table, REST API, executor authority model (runs as source workflow owner, org-scoped, exposes only curated outputs), and client-side deploy UI/block-registry overlay.
  • Chat performance fix: Animated Streamdown tree is now drained and remounted as static after streaming settles, with a fade cutoff at 6 000 chars and word-level span granularity to eliminate long-task buildup across long sessions.

Confidence Score: 4/5

Safe to merge with one fix recommended before the next release: the spread order in mcpAuthGuarded should be inverted so that a caller-supplied fetchFn: undefined falls back to the SSRF guard rather than removing it.

The mcpAuthGuarded wrapper has a subtle spread-order issue where a fetchFn: undefined in caller options silently removes the SSRF guard. No current production caller exercises this path, so there is no live exploit, but it is a latent security gap in the function that is supposed to be the mandatory SSRF enforcement point for all MCP OAuth exchanges. All other security changes look correct and well-tested.

apps/sim/lib/mcp/oauth/auth.ts (spread order in mcpAuthGuarded) and apps/sim/app/api/tools/google_drive/download/route.ts (NaN handling for metadata.size before assertKnownSizeWithinLimit).

Security Review

  • MCP OAuth SSRF guard bypass via spread order (apps/sim/lib/mcp/oauth/auth.ts): { fetchFn: createSsrfGuardedMcpFetch(), ...options } places the guard before the caller's spread, so a caller-supplied fetchFn: undefined silently overwrites the guard with undefined. Current production callers do not pass fetchFn, so there is no live exploit path, but future callers could inadvertently disable SSRF protection for every URL touched during an MCP OAuth exchange. Safe fix: { ...options, fetchFn: options?.fetchFn ?? createSsrfGuardedMcpFetch() }.
  • EmailBison createSubscription and deleteSubscription now correctly validate apiBaseUrl via DNS and use secureFetchWithPinnedIP — no gaps found in the webhook provider.
  • Drive/OneDrive/Slack/STT download routes correctly add maxResponseBytes and early size guards; the parseInt(metadata.size) NaN edge case in the Drive route degrades the early guard but the stream-level limit still holds.
  • isSafeHttpUrl is now a shared utility used before opening untrusted workflow-output URLs, correctly rejecting javascript:, data:, blob:, and vbscript: schemes.
  • Custom-block authority resolution is correctly scoped to the consumer's org; the source workflow's internal spans, name, and trace are stripped before returning output to the consumer.

Important Files Changed

Filename Overview
apps/sim/lib/mcp/oauth/auth.ts New mcpAuthGuarded wrapper defaults all MCP OAuth exchanges to the SSRF-guarded fetch, but the spread order { fetchFn: guard, ...options } allows a caller-supplied fetchFn: undefined to silently overwrite the guard.
apps/sim/lib/webhooks/providers/emailbison.ts Both createSubscription and deleteSubscription now validate apiBaseUrl via DNS and use secureFetchWithPinnedIP; the AlreadyLoggedError sentinel cleanly avoids double-logging.
apps/sim/lib/workflows/custom-blocks/operations.ts Core data-access and authorization layer for deploy-as-block; well-defended with org-scoping and ownership checks, though publishCustomBlock lacks retry logic for the rare shortId collision case.
apps/sim/executor/handlers/workflow/workflow-handler.ts Custom block execution runs under the source workflow owner's identity; correctly skips the same-workspace assertion, strips internal child spans on success/failure, and carries only aggregate cost to the parent.
apps/sim/app/api/custom-blocks/route.ts REST endpoints for listing and publishing custom blocks; correctly gates on workspace admin, feature flag, and enterprise plan, with authorization checks re-verified inside operations layer.
apps/sim/app/api/custom-blocks/[id]/route.ts PATCH/DELETE for managing a custom block; authorization correctly requires admin on the source workflow's workspace (not just any org workspace).
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx Streaming performance fix: replaces the permanently-live animated Streamdown tree with a drain-then-swap lifecycle, cuts span count via sep: 'word', and caps fade animation at 6000 chars to eliminate long-task bloat.
apps/sim/app/api/tools/google_drive/download/route.ts Adds maxResponseBytes and early assertKnownSizeWithinLimit for size bounding; the parseInt path for metadata.size silently passes NaN to the guard, making it a no-op for malformed size strings.
apps/sim/app/_styles/globals.css Changes ::selection from solid-brand + forced white to a translucent muted tint; functionally justified (fixes invisible text on styled mirrors), but adds global CSS variables where the literal value could be inlined.
packages/db/schema.ts Adds custom_block table with cascade-delete on org and workflow, unique (organizationId, type) index, and JSON outputs column; migration matches the schema definition.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Consumer Workflow Block\n custom_block_xyz] -->|canHandle| B{isCustomBlockType?}
    B -->|yes| C[getCustomBlockAuthority\nscoped to consumer org]
    C -->|not found / disabled| D[throw: block no longer available]
    C -->|found| E[Load authority:\nworkflowId, ownerUserId,\nexposedOutputs]
    E --> F[Load source workflow\nas owner, latest deployment]
    F --> G[remapCustomBlockInputKeys\nid to name for Start fields]
    G --> H[Run child Executor\nunder owner userId+workspace+env]
    H -->|success| I[projectCustomBlockOutput\ncurated exposed outputs only\naggregate child cost]
    H -->|failure| J[buildCostCarrierSpans\ncost only, no internals]
    I --> K[Return to parent executor]
    J --> L[throw ChildWorkflowError\ngeneric message to consumer]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Consumer Workflow Block\n custom_block_xyz] -->|canHandle| B{isCustomBlockType?}
    B -->|yes| C[getCustomBlockAuthority\nscoped to consumer org]
    C -->|not found / disabled| D[throw: block no longer available]
    C -->|found| E[Load authority:\nworkflowId, ownerUserId,\nexposedOutputs]
    E --> F[Load source workflow\nas owner, latest deployment]
    F --> G[remapCustomBlockInputKeys\nid to name for Start fields]
    G --> H[Run child Executor\nunder owner userId+workspace+env]
    H -->|success| I[projectCustomBlockOutput\ncurated exposed outputs only\naggregate child cost]
    H -->|failure| J[buildCostCarrierSpans\ncost only, no internals]
    I --> K[Return to parent executor]
    J --> L[throw ChildWorkflowError\ngeneric message to consumer]
Loading

Reviews (1): Last reviewed commit: "fix(mcp): make SSRF-guarded fetch struct..." | Re-trigger Greptile

Comment thread apps/sim/lib/mcp/oauth/auth.ts Outdated
--brand-accent: #33c482;
--brand-accent-hover: #2dac72;
--selection: #1a5cf6;
--selection-muted: #1a5cf647;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 This edit modifies globals.css, which the team style rule reserves for cases where a local component file genuinely cannot carry the change. ::selection is a global pseudo-element that cannot be scoped to a single component, so the change is functionally justified — but the new --selection-muted variable is only consumed by ::selection, so it could be inlined as a literal there instead of adding two new global custom properties.

Suggested change
--selection-muted: #1a5cf647;
/* --selection-muted: #1a5cf647; — inline in ::selection instead of adding a global variable */

Rule Used: Avoid editing the globals.css file unless absolute... (source)

Learned From
simstudioai/sim#367

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +285 to +292
throw new CustomBlockValidationError('You can only publish a workflow from its own workspace')
}

const ws = wf.workspaceId ? await getWorkspaceWithOwner(wf.workspaceId) : null
if (!ws?.organizationId || ws.organizationId !== organizationId) {
throw new CustomBlockValidationError('Workflow does not belong to this organization')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No retry for shortId collision in publishCustomBlock

generateShortId(10) produces a 10-character identifier, which makes collisions extremely unlikely. However, if the custom_block_organization_type_unique index rejects the insert due to a collision, the error propagates as an unhandled DB constraint violation (HTTP 500), not a user-friendly message. Given that the existing guard already prevents the same workflow from being published twice, the only remaining collision path is a random-ID clash. A simple retry or an explicit catch of unique-constraint errors would surface a clean error rather than leaking internal DB error text.

Comment thread apps/sim/app/api/tools/google_drive/download/route.ts Outdated
… time-based reveal pacing (#5417)

* improvement(chat): eased stick-to-bottom glide for streaming chat

* improvement(chat): time-based word-at-a-time reveal pacing

* improvement(chat): harden smooth-chase against reentrant kick/cancel during a step

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f456ed9. Configure here.

Spread order previously let an explicit fetchFn (including fetchFn: undefined)
in options silently disable the SSRF-guarded default. Fallback is now applied
after the spread so the guard always wins unless a real override is passed.

fix(tools): handle non-numeric Drive file size in early size check

Guard the pre-download size check against a malformed metadata.size string
so it's skipped explicitly instead of relying on an incidental NaN no-op;
the streaming cap on the actual download still enforces the limit either way.
…es (#5422)

* improvement(compare): accuracy and consistency pass on comparison pages

Cross-checked facts across all comparison pages against live sources,
corrected a few internal inconsistencies, and added one new fact category.
No schema or rendering changes beyond a single new row.

* fix(compare): align a parity check and a stale source citation

* improvement(compare): cross-reference Sim's permission-groups system from rbac

* improvement(compare): sharper bottom-line reasoning, minor emphasis fixes

Reworded a few competitor headline claims to reflect genuine product
positioning rather than a narrow feature, and gave a couple of Sim's
own already-documented capabilities (model reach, integration count,
live collaboration) more prominent placement.

* improvement(compare): verify every cited source is live, fix a few dead links

* fix(compare): correct mcpSupport boolean-icon misparse in power-automate.ts
)

Matches the emcn ChipLink pattern already used by sibling landing not-found pages instead of a hand-rolled Link with raw Tailwind classes.
…g from the table (#5425)

dataTables and richTextEditor are required fact fields, already researched and
filled in for Sim and every competitor, but neither was ever added to the row
list the table actually renders.
@waleedlatif1 waleedlatif1 merged commit 616a71d into main Jul 5, 2026
33 checks passed
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.

2 participants