Commit Graph
5696 Commits
Author SHA1 Message Date
Waleed dda602a367 improvement(tests+ci): phase 3 — shared-mock convergence completion and CI runner-minute cuts (#5875)
* chore(ci): cut redundant runner minutes — dedup promotion-PR test runs, companion-pr-check concurrency, right-size trivial jobs

- ci.yml: new dedup-promotion gate skips the pull_request test-build on
  staging/main-headed promotion PRs only when the merge tree provably equals
  the head tree (empty base delta over the merge base) AND the push-event run
  at the same sha passed its test jobs (polled). Fail-open on any error/
  timeout/failure, job-level skip only (skipped job reports Success); verified
  no required status checks are configured on main/staging rulesets.
  Measured 39 duplicate PR runs / 5.15 days (~227/mo) at ~7.1 min each on
  8vcpu (~57 vcpu-min), probe costs ~9 vcpu-min worst case on 2vcpu.
- companion-pr-check.yml: per-PR concurrency group with cancel-in-progress so
  superseded synchronize/edit runs stop; no paths filter (check depends on PR
  body + cross-repo state, not changed files).
- detect-version and check-docs-changes: 4vcpu -> 2vcpu Blacksmith runners
  (pure shell / depth-2 checkout + path filter only).

* improvement(testing): complete stateful shared mocks for env, urls, redis-config, environment-utils

Shared mock infrastructure for vitest isolate:false convergence:
- packages/testing/src/mocks/env.mock.ts: stateful envMock (live env proxy, setEnv/resetEnvMock, process.env fallback)
- packages/testing/src/mocks/urls.mock.ts: complete urlsMock with real-behavior default impls + resetUrlsMock
- packages/testing/src/mocks/redis-config.mock.ts: adds getRedisConnectionDefaults + resetRedisConfigMock
- packages/testing/src/mocks/environment-utils.mock.ts: new environmentUtilsMock + fns + reset
- contract tests: env.mock.test.ts, urls.mock.test.ts, redis-config.mock.test.ts, environment-utils.mock.test.ts
- packages/testing/src/mocks/index.ts: barrel exports
- apps/sim/vitest.setup.ts: global installs for env, urls, redis, environment/utils
- real-module tests unmocked: lib/core/config/env.test.ts, lib/core/config/redis.test.ts, lib/core/utils/urls.test.ts, tools/index.test.ts (urls)
- stubEnv/process.env fallout migrated to setEnv: lib/webhooks/providers/{revenuecat,rootly,instantly}.test.ts, app/api/auth/oauth2/authorize/route.test.ts

* improvement(tests): drop redundant local mocks in executor/tools/providers and misc dirs (shared-worker readiness)

* improvement(tests): drop redundant local mocks in app routes (shared-worker readiness)

* improvement(tests): drop redundant local mocks in lib (shared-worker readiness)

* fix(ci+testing): live base-tip recheck before dedup skip; prod-aware urls mock fallbacks

- the dedup gate re-verifies merge-tree equivalence against the LIVE base
  tip at decision time, closing the window where the base branch gains real
  commits during the poll (frozen BASE_SHA check alone was stale)
- the urls mock's getBaseUrl protocol prefix and getBaseDomain parse
  fallback now follow the shared isProd flag, mirroring the real module

* fix(ci+testing): fail-closed nojobs fallback in dedup gate; TLS-aware redis defaults mock

- the dedup gate no longer infers coverage from overall run conclusion when
  no 'Test and Build /' jobs match — a renamed or skipped test job now runs
  the tests instead of skipping them
- the shared getRedisConnectionDefaults mock mirrors the real TLS resolution
  (rediss:// to a raw IP requires REDIS_TLS_SERVERNAME and yields
  tls.servername)

* fix(ci): keep polling while nested test jobs have not appeared yet

An in-progress push run lists its reusable-workflow jobs only after the
caller starts; nojobs is now terminal (fail closed) only once the run has
completed without them.
2026-07-22 19:04:17 -07:00
Waleed 9d8e14ce9f improvement(tests): converge env-flags mocks onto a complete shared mock (#5871)
* improvement(tests): converge env-flags mocks onto a complete shared mock

* fix(tests): drop the repo's only bare vi.mock automock

A bare vi.mock('drizzle-orm') automock colliding with factory mocks of the
same module in a shared worker corrupts vitest's mock registry (upstream
vitest-dev/vitest#10290 / #10145, reproduced in isolation). The global
factory mock already covers this suite.

* chore(tests): drop defensive resets in non-mutating suites, merge sequential setEnvFlags calls

* fix(tests): run providers/utils cases sequentially over shared env-flags state
2026-07-22 17:31:18 -07:00
Waleed 02bc8f1828 feat(ci): warm Next.js builds via Turbopack persistent cache on a sticky disk (#5869)
* feat(ci): warm Next.js builds via Turbopack persistent cache on a sticky disk

- enable experimental.turbopackFileSystemCacheForBuild behind
  NEXT_TURBOPACK_BUILD_CACHE so only the CI check build opts in; production
  image builds stay on the default cold path until the feature stabilizes
- mount ./apps/sim/.next/cache as a Blacksmith sticky disk (cache-mount)
  instead of actions/cache: the turbopack cache is ~5 GB, which a sticky disk
  mounts in ~1s while an actions/cache round-trip would eat the win
- measured locally: 105s cold compile vs 22s warm (4.8x)

* chore(ci): drop the superseded actions/cache comment and restore trailing newline
2026-07-22 17:10:24 -07:00
Waleed 2b5a92a3c8 feat(auth): org session policies — lifetime/idle limits, org-wide revocation (#5862)
* feat(auth): org session policies — lifetime/idle limits, org-wide revocation, cookie-cache versioning

* refactor(auth): consolidate session-policy clamp semantics, shared security-policy version module, canonical bounds, docs

* polish(session-policy): cleanup pass — muted field labels, spinner reset, state tracker, response-seeded baseline, comment trims

* fix(session-policy): govern member sessions by membership (closes revoke cookie-cache hole), normalize createdAt, remount on org switch, sync audit mock

* fix(session-policy): clamp pre-join sessions on invite acceptance, normalize expiresAt, sync unified nav test

* fix(session-policy): invalidate membership cache on removal/transfer, spare impersonator sessions in revoke-all, raise idle floor to 2x cookie window

* fix(session-policy): resolve governing org by membership only — activeOrganizationId goes stale across transfer/leave

* fix(session-policy): atomic policy save + eager clamp, asymmetric membership TTL, admin-add cache invalidation

* fix(session-policy): org-scoped cookie version string, atomic revoke delete+bump

* fix(session-policy): plan-gate effective policy so downgraded orgs stop enforcing automatically

* chore(session-policy): drop dead bumpSecurityPolicyVersion helper — call sites bump transactionally

* fix(session-policy): unify join paths on applySessionPolicyToNewMember; final audit polish (dead exports, response bound, test name)
2026-07-22 16:42:59 -07:00
WaleedandMarcus Chandra 8cce661a37 feat(api): proxyUrl for residential/custom proxy egress on the API block (#5867)
* feat(api): add proxyUrl for residential/custom proxy egress on the API block

The HTTP/API block egresses from the app runtime's fixed datacenter IPs via
secureFetchWithPinnedIP, so targets behind Cloudflare/WAF that block datacenter
IPs (e.g. state .gov license portals) return 403/429 even when the identical
request works from a browser. There was no way to route a request through a
residential/custom proxy.

Add an optional `proxyUrl` field (Advanced) to the API block. When set, the
request routes through the given http:// proxy so it egresses from that proxy's
IP.

Security:
- validateAndPinProxyUrl resolves the proxy host's DNS and blocks
  private/reserved/loopback IPs (same SSRF guard as target URLs), then pins the
  connection by rewriting the host to the resolved IP (creds/port preserved),
  closing the DNS-rebinding window.
- Restricted to the http: proxy scheme (https/socks rejected) so host pinning is
  safe without breaking TLS-to-proxy SNI.
- Target-IP pinning is intentionally bypassed when a proxy is active (the proxy
  resolves the target); target URL validation still runs.

Threaded block field -> http tool param -> formatRequestParams ->
executeToolRequest (validate + pin) -> secureFetchWithPinnedIP, which swaps its
pinned Node agent for HttpsProxyAgent/HttpProxyAgent (keyed off target protocol)
when proxyUrl is set.

* docs(api): document the Proxy URL advanced field and steer proxy credentials to env vars

* fix(api): reject loopback/private proxy hosts unconditionally, closing the self-hosted rebinding gap

* chore(api): tighten proxy-path inline comments

---------

Co-authored-by: Marcus Chandra <mzxchandra@gmail.com>
2026-07-22 16:24:31 -07:00
Waleed 21ac7b1bba improvement(tests): db-mock migration tranche 4 — billing, webhooks/execution/logs, routes/misc (final) (#5866)
* improvement(tests): db-mock migration tranche 4 — billing, webhooks/execution/logs, routes/misc (final)

* fix(tests): route agent-handler MCP server rows through queueTableRows
2026-07-22 15:51:18 -07:00
Waleed 51307c40c0 improvement(tests): db-mock migration tranche 3 — lib/workflows, lib/copilot remainder, ee/core/misc (#5864)
* improvement(tests): db-mock migration tranche 3 — lib/workflows, lib/copilot remainder, ee/core/misc

* improvement(tests): use the shared notLike operator in idempotency cleanup suite
2026-07-22 15:29:58 -07:00
Waleed 52659d4a89 improvement(tests): db-mock migration tranche 2 — copilot, mothership, workspaces, connectors, mcp (#5863)
* improvement(tests): db-mock migration tranche 2 — copilot, mothership, workspaces, connectors, mcp

* fix(testing): drain unconsumed ...Once overrides in resetDbChainMock

vi.clearAllMocks clears call history only — a ...Once override queued by a
previous test but never consumed survived into the next test. resetDbChainMock
now mockReset()s every shared spy and stable wrapper, which restores the
original implementation AND drains once-queues.
2026-07-22 15:17:43 -07:00
Waleed 62a8ce4953 improvement(tests): db-mock migration tranche 1 — knowledge, billing/org, workflows/background (#5861)
* improvement(tests): migrate knowledge, billing/org, and workflows/background suites off private @sim/db factories

* improvement(tests): db-mock migration tranche 1 — knowledge, billing/org, workflows/background

- migrate 19 suites off private vi.mock('@sim/db') factories onto the shared
  dbChainMock + queueTableRows API (net ~-1,260 lines of bespoke chain
  plumbing); resolves the known shared-worker rival pairs (knowledge
  processing-queue vs api utils; billing polluters; persistence/utils vs
  schedules/deploy)
- add .for() to the mock's limit builder (drizzle .limit(1).for('update'))
  with a contract test
- document the join-table queue fallback footgun on queueTableRows
2026-07-22 14:53:17 -07:00
Waleed c083be9def improvement(auth): bump better-auth to 1.6.23 and add trusted-proxy client IP resolution (#5857)
* improvement(auth): bump better-auth to 1.6.23 and add trusted-proxy client IP resolution

* chore(billing): record checkout-scope mirror re-verification against @better-auth/stripe 1.6.23

* chore(deploy): expose AUTH_TRUSTED_PROXIES in docker-compose.prod and Helm chart
2026-07-22 14:41:02 -07:00
Waleed 49d3804bee fix(ci): save the Next.js build cache every run instead of freezing it at the lockfile key (#5859)
* fix(ci): save the Next.js build cache every run instead of freezing it at the lockfile key

The cache key was only runner.os + bun.lock hash, and GitHub caches are
immutable per key: the first run after a lockfile change saved the cache
once, then every later run hit the primary key and skipped the save
('Cache hit occurred on the primary key ... not saving cache'), so builds
compiled against a cache stale since the last lockfile bump. Suffix the
key with the commit SHA so each run saves its refreshed cache, and
restore via prefix match to the most recent entry.

* fix(ci): make the Next.js cache key unique per run attempt so reruns can save too
2026-07-22 14:28:38 -07:00
Waleed 3215a12da9 improvement(testing): consolidate @sim/db mocks into one table-aware chain mock (#5856)
* improvement(testing): consolidate @sim/db mocks into one table-aware chain mock

- back databaseMock and dbChainMock with the SAME db instance so a module
  bound to either export hits identical chain fns — rival-mock divergence
  between the two @sim/testing db mocks is structurally impossible now
- add queueTableRows(table, rows): FIFO per-table select routing keyed by
  schema-mock table identity, consumed at where() materialization and
  resolved by every downstream terminal (limit/orderBy/groupBy/for/joins)
- delete createMockDb (duplicate chain implementation, no external users)
- migrate the five suites that hand-rolled table routing + databaseMock
  delegation (billing plan/usage/usage-log, admin dashboard-organizations,
  workspaces/utils) onto queueTableRows; net -295 lines
- add a contract test for the mock itself and a test script to
  @sim/testing so its tests actually run under turbo

* fix(testing): harden table routing — join-table queues, direct-await from, mutation isolation

- track the chain's tables as a list (from + joins) so rows queued for a
  join-only table route correctly; from-table queue checked first
- make the from/join builder a lazy thenable so awaiting a select with no
  where clause resolves queued rows (dequeue at await, never double-consumed)
- update/delete/set clear the routing context so a mutation's where() can
  never consume rows queued for a select
- document the left-to-right chain-construction assumption; contract tests
  for all three behaviors

* fix(testing): close routing over each chain's own tables for direct-await builders

* refactor(testing): move all chain routing state into per-chain closures

- shared dbChainMockFns entries become pure spy/override ports: their default
  implementation returns a sentinel that chain-local builders replace, while
  any mock* override on the spy wins verbatim
- each select().from() captures its own immutable table list; where(),
  joins, terminals, and direct awaits all resolve through that closure, so
  partially-built chains for different tables interleave without cross-talk
- no module-level routing state remains

* fix(testing): lazy queue consumption at resolution and wrapper restore on reset

- each chain holds one lazy rows supplier: the queued set is dequeued only
  when a default thenable actually resolves, so a chain answered by a
  per-test terminal override leaves its queued rows for the next chain
- resetDbChainMock also mockReset()s the stable db entry-point wrappers so
  direct overrides on databaseMock.db.* cannot outlive a suite
2026-07-22 14:15:44 -07:00
Theodore LiandClaude Opus 4.8 ae7b5eff6c feat(copilot): service account setup & reconnect in chat (#5786)
* feat(copilot): add service_account_get_setup_link handler

Resolves a loosely-specified integration name to the catalog slug whose
detail page mounts ConnectServiceAccountModal, and returns
`/integrations/{slug}?connect=service-account`. The agent surfaces it via
the existing <credential type="link"> tag, so the user gets a Connect
button and supplies the key material in Sim's own form — the agent never
handles the secret.

Exact matches beat fuzzy ones so a caller naming a specific service lands
on it (gmail stays Gmail rather than collapsing to Drive), and family
names resolve through an explicit canonical map rather than to whichever
member sorts first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds

* fix(copilot): reject service account ids in oauth_get_auth_link

The fuzzy provider match falls back to substring containment, so
`slack-custom-bot` contains `slack` and resolved to the Slack OAuth
service. The tool then returned a personal-OAuth authorize URL and
reported success — a user who asked for a shared custom bot got a
Connect button that linked their own account instead. Every service
account id degraded this way (notion-, salesforce-, zoom-, linear-),
always silently.

Guard runs before the fuzzy pass and points at
service_account_get_setup_link. Keys off the id being a service-account
id, not off the integration offering one, so `slack` and `notion` still
resolve for OAuth.

Moves the narrowing predicate out of the integration catalog module so
callers that need only the predicate skip the integrations.json load and
the OAUTH_PROVIDERS walk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds

* feat(copilot): open the service account form in-chat instead of linking out

The tool handed back a /integrations/{slug}?connect=service-account URL,
so accepting the agent's offer navigated away from the conversation that
asked for the credential. Adds a `service_account` credential tag that
mounts ConnectServiceAccountModal over the chat; setup_url stays as the
headless/MCP fallback.

The tag carries a provider and no value — the secret is typed into Sim's
own form and never enters the transcript — so the validator gets a branch
alongside secret_input/sim_key rather than falling through to the
value-required check.

Extracts useServiceAccountConnectTarget so the chat and the integrations
page share one source of truth for the connect label and the preview
gate. Custom Slack bots ride the slack_v2 flag; without the shared gate
the chat would have surfaced a setup form the integrations page hides.

Modal is lazy-loaded off the deep path (not the barrel) to keep three
provider-specific setup forms out of the chat's initial chunk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds

* fix(copilot): gate service account tool on the same preview flag as the UI

The in-chat connect button hides itself when the provider's gating block
is preview-hidden (a custom Slack bot needs slack_v2). The tool didn't
check this, so it returned success for slack-custom-bot even when slack_v2
was preview-gated off — the agent said "here's the setup form" and the
button silently rendered nothing, leaving the user with no form at all.

Adds getServiceAccountGatingBlockType as the single source for the
provider→gating-block mapping, consumed by both the tool (server-side, via
getBlockVisibilityForCopilot) and the connect hook (client overlay). When
the gating block is hidden the tool now fails with a fall-back-to-OAuth
message instead of promising an invisible form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds

* feat(copilot): make the tool own service-account discovery

Removes the VFS auth-metadata exposure and returns connectNoun from the
service_account_get_setup_link result instead. The VFS aggregate was a
second, viewer-independent source of truth that couldn't agree with the
per-viewer preview gate (it always hid slack-custom-bot, even for viewers
with slack_v2 revealed, while the tool accepts it for them). The tool now
resolves the provider, applies the per-viewer gate, and returns either the
in-chat button + connectNoun or a fall-back-to-oauth error — one source of
truth. connectNoun stays DRY via getServiceAccountConnectNoun, shared with
the connect-button label.

* feat(copilot): make service-account setup a direct tag, no tool

The agent now emits the service_account credential tag directly from
intent — like secret_input — instead of round-tripping through a tool.
Removes service_account_get_setup_link (handler, registration, display
title, Go tool def) and restores auth.serviceAccount as the VFS discovery
field so the agent knows which providers support a service account.

The link-vs-tag distinction was the wrong axis: only oauth needs a tool,
because its button carries a minted URL that can't be reconstructed. The
service_account tag carries just a provider name the agent already knows,
so it needs no tool — discovery lives in the VFS (auth.serviceAccount,
GA-only, so slack's preview-gated custom bot is never proactively
offered), and the per-viewer gate lives in the renderer, which renders
nothing when a provider isn't available for the viewer (no OAuth
fallback — a shared credential and a personal one are different intents).

oauth_get_auth_link's service-account-id guard now points at the tag.

* feat(copilot): support service-account reconnect from chat

Reconnect had no service-account path — it required oauth_get_auth_link
and a link tag for every repair, so rotating a workspace service account
either errored or pushed the user through OAuth.

The service_account tag now takes an optional credentialId; when present
the renderer opens the modal in reconnect mode (rotates the secret on that
credential in place, id preserved) and labels the button "Reconnect X".
credentials.json now carries each credential's type (oauth vs
service_account) so the agent can branch: service accounts reconnect via
the tag + credentialId, oauth via oauth_get_auth_link as before.

* fix(copilot): coherent service-account rejection in oauth_get_auth_link

Review round on #5786:
- The service-account-id guard threw into the generic catch, which
  overwrote its recovery hint with a "connect manually" message and a
  workspace oauth_url — contradictory signals. It now returns a coherent
  failure directly, before the try, with no oauth_url.
- Normalize spaces/underscores before the check so a readable form
  ("slack custom bot", "google service account") is caught too, not
  passed to the fuzzy OAuth resolver.
- Remove listServiceAccountIntegrationNames — dead after the tool was
  removed (its only caller was the deleted handler's error copy).

* fix(copilot): service-account discovery must un-gate after the block GAs

Review round on #5786: describeServiceAccountForOAuthProvider used
`getBlock(...)?.preview ?? true`, which treats a GA'd gating block — one
that dropped its `preview` flag, exactly slack_v2's documented migration —
as still gated, so the custom bot would stay omitted from VFS discovery
forever after GA even though the UI shows it. Reuse the canonical
isHiddenUnder(null, block) predicate instead, so a non-preview block is
visible. Adds service-account-gate.test.ts covering preview → omit, GA →
include, and missing → fail-closed with a mocked getBlock (the block
registry is globally stubbed, so the real slack_v2 preview flag isn't
observable through serializeIntegrationSchema).

* fix(copilot): align SA resolver normalization and reject blank credentialId

Review round on #5786:
- resolveServiceAccountIntegration only lowercased/trimmed, but the
  oauth_get_auth_link guard normalizes spaces/underscores to hyphens
  before rejecting a service-account id and steering the agent to a
  service_account tag. The chat renderer then couldn't resolve those same
  readable forms ("slack custom bot", "notion_service_account") and
  rendered nothing. Apply the same normalization to the id lookups (raw
  query still used for display-name matches).
- service_account tag validation rejected a blank provider but allowed a
  whitespace-only credentialId, which is truthy — the renderer took the
  reconnect path and tried to rotate a non-existent credential. Reject a
  blank/whitespace credentialId.

* refactor(credentials): route the editor SA picker through the canonical connect hook

The workflow-editor credential selector (from #5800's merged picker) resolved
its service-account setup surface inline and mounted the modal with NO preview
gate — so a `credentialKind: 'service-account'` picker would offer a custom-bot
setup even when slack_v2 is preview-gated off, the leak the integrations page
and chat already guard against.

Route it through the shared useServiceAccountConnectTarget hook (the same
resolver chat and the integrations page use): suppress the setup action when
`hidden`, and use the hook's vendor-accurate label ("Add private app token",
"Set up a custom bot") as the default connect-row copy. Existing service
accounts stay selectable; the per-block `credentialLabels.serviceAccountConnect`
override still wins. One resolver now backs all three SA connect surfaces.

* docs(add-block): document credentialKind and the service-account picker

The add-block skill had no mention of credentialKind — the mechanism (#5800)
that controls whether an oauth-input offers OAuth, service-account, or a merged
picker — and its example was a plain oauth-input mislabeled "Service Account".
Documents the three credentialKind modes, that a default oauth-input already
lets users select an existing service account (they fold in), and the
credentialLabels / allowServiceAccounts companions. Regenerates the .claude and
.cursor projections.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:06:29 -04:00
WaleedandMarcus Chandra 6f33a9485b feat(workflows): IDE-style reference viewer for workflows (#5854)
* feat(workflows): add IDE-style reference viewer for workflows

Adds a "Show references" viewer so you can see how workflows connect:
which workflows call a given workflow ("Used by") and which workflows it
calls ("Uses"), rendered as recursive, clickable trees.

- Opened via Cmd/Ctrl+click on a sidebar workflow row and a "Show
  references" context-menu item.
- Resolves references through both the workflow / workflow_input blocks
  (reusing isWorkflowBlockType) and published custom blocks
  (custom_block_* -> source workflow), scoped to the workspace.
- Builds the whole workspace reference graph once from live workflow_blocks
  state; cycle-safe DFS marks A->B->A loops as (cycle) leaves.
- Contract-bound GET /api/workflows/[id]/references with workspace-level
  authz; React Query hook gated to fetch only when the modal opens.
- Unit tests for the pure graph/tree logic (cycles, self-refs, dangling
  drop, custom-block + workflow_input resolution) and route tests
  (401/400/403/200).

* fix(workflows): correct reference resolution for active mode, cycles, cache, and graph size

Addresses review findings on the reference viewer:

- Resolve the workflow-block child via resolveActiveCanonicalValue (the
  shared SOT) instead of basic-first `||`, so an advanced-mode block whose
  old basic workflowId value lingers resolves to the active manual value.
- Keep self-references (A -> A) and render them as a cycle leaf instead of
  dropping the edge, matching the cycle-safe viewer's purpose.
- Set the references query staleTime to 0 so reopening the always-mounted
  modal refetches live editor state instead of serving a stale cached graph.
- Bound converging paths: a node already expanded elsewhere in the tree is
  emitted once more as a plain leaf (edge stays visible) rather than
  re-expanded, so a densely reconverging graph can't grow exponentially.

* improvement(workflows): align reference viewer auth, coverage, and UI with platform conventions

- authorize via authorizeWorkflowByWorkspacePermission and derive the
  workspace server-side (404/403 semantics; drops the client-supplied
  workspaceId query param from the contract, hook, and modal)
- add workflow-tool call edges: workflow_input tools inside tool-input
  sub-blocks now appear in both trees; non-call selector shapes stay
  deliberately excluded (documented against remap-internal-ids)
- restore native cmd/ctrl+click open-in-new-tab on sidebar workflow rows;
  references stay reachable from the context menu
- mount ReferencesModal on demand per row, deleting the prevIsOpen reset,
  the enabled knob, and the staleTime-0 workaround (now 30s)
- align the tree with design tokens (--text-icon, --surface-hover, px-4
  text gutter) and drop the hardcoded brand hex
- escape LIKE wildcards in the custom_block_ prefix match; import
  MAX_CALL_CHAIN_DEPTH instead of mirroring it; remove dead fallbacks,
  the duplicate not-found scan, and the redundant custom-block row map

* improvement(workflows): final polish on the reference viewer

- drop the vestigial isOpen prop (conditional mount owns visibility)
- unify on the emcn Workflow icon in tree rows
- inline the static className and derive nodes without an annotation
- remove one restating test comment

* fix(workflows): resolve tool references by active canonical mode and keep the reference cache live

- workflow_input tools inside tool-input now resolve basic/advanced via the
  index-scoped canonicalModes override, mirroring execution (Cursor finding)
- staleTime back to 0: no mutation invalidates this key, so a reopen must
  background-refetch; on-demand mounting keeps the cached tree painting
  instantly (Greptile P1)
- modal header uses the em-dash label-entity convention; tree items carry
  aria-level instead of a static aria-selected

* fix(workflows): cover legacy workflow-typed tools and retry depth-truncated expansions

- toolInputCallees matches both workflow tool type spellings via
  isWorkflowBlockType and passes the tool's own type as the legacy
  canonicalModes fallback, matching providers/utils resolution
- a depth-capped expansion no longer poisons the expanded set, so a
  shallower path re-expands the node in full (Cursor finding)
- the allowed-but-workspaceless auth branch now returns 403, not the
  authz result's 200
- tests: legacy tool type + per-tool index-scope isolation, diamond
  re-expansion with a real subtree, depth ceiling, shallow-path retry

---------

Co-authored-by: Marcus Chandra <mzxchandra@gmail.com>
2026-07-22 14:03:07 -07:00
Waleed 5338485ac1 feat(sidebar): add Slack Community link to help dropdown (#5858)
* feat(sidebar): add Slack Community link to help dropdown

* chore(sidebar): use existing Slack community invite link
2026-07-22 13:53:58 -07:00
Theodore Li d7d42faff8 fix(mothership): stop-button transitions freeze in place (#5838)
* fix(mothership): freeze the transcript on user stop instead of settle-scrolling

* fix(mothership): swap stopped row into the shimmer slot and floor the sizer min-height

* fix(mothership): detach auto-scroll on stop and dead-band the sizer floor

* improvement(mothership): net-zero settle — equal tail regions, drained floor, one growth signal

* fix(mothership): follow the post-stop drain to the end instead of freezing

* fix(mothership): single stopped tail region and drain cleanup

* fix(mothership): clamp-aware chase interrupt so the floor drain can't park the settle follow

* fix(mothership): park the chase only on real upward top moves, not growth

* improvement(mothership): stack the stopped status above the actions row

* improvement(mothership): compact stopped-turn tail with the original 10px rhythm

* fix(mothership): single drain cadence and a settle-window gesture kill switch

* fix(mothership): debt-aware drain fast-path and settle-window handle retention
2026-07-22 16:53:35 -04:00
Theodore LiandClaude Fable 5 b49fe16dae improvement(logs): show Redacting status while PII masking runs (#5855)
* improvement(logs): show Redacting status while log-persist PII masking runs

Log-stage PII redaction happens at persist time and can take minutes on large
payloads, during which the Logs page showed the run as Running long after
execution finished. The persist path now flips the log row to 'redacting'
(guarded on 'running' so a concurrent cancellation is never clobbered) right
before the masking work starts — only when the logs redaction stage is
actually enabled — and the terminal update overwrites it with the final
status. The Logs UI renders an amber non-filterable Redacting badge (row +
details sidebar via the shared STATUS_CONFIG), keeps polling the detail query
during the phase, and keeps resolving live progress markers. No migration:
status is a free-text column, and the contract already types it as string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(logs): never let the cosmetic redacting write abort log finalization

Review finding: the status flip was awaited without failure isolation, so a
transient DB error there rejected applyPiiRedaction before masking and the
terminal update never ran. The write is display-only; catch and warn instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:43:09 -04:00
Waleed fad9728cbd improvement(tests): make vitest suites state-safe with auto-unstub of env/global stubs (#5853)
* improvement(tests): make vitest suites state-safe with auto-unstub of env/global stubs

- add unstubEnvs/unstubGlobals to vitest config so vi.stubEnv/vi.stubGlobal
  are restored after every test
- move module-scope fetch/crypto/window stubs into beforeEach across executor,
  data-drains, and knowledge suites so they hold under auto-unstub (several
  suites were silently hitting live Datadog/BigQuery/Snowflake/OpenAI/Anthropic
  endpoints when their module-scope stubs were cleared)
- converge billing/workspaces/tools/hooks suites onto the shared @sim/testing
  mock instances and namespace spies instead of rival file-local vi.mock
  factories, with explicit beforeEach setup and afterAll restore
- give lib/core suites private module instances via vite query-suffix imports
  where module-level state bakes env at import time
- stub auth-client/NEXT_PUBLIC_APP_URL in suites that crashed at collection
  without a local .env

Groundwork for eventually running the suite with isolate: false; no CI
behavior change (isolation stays on).

* fix(tests): complete databaseMock restore and route selectDistinctOn

- restoreGlobalDb now mockReset()s entries whose original implementation was
  undefined instead of leaving the chain-mock delegation installed
- add selectDistinctOn to the setup-level createMockDb and to the dashboard
  suite's delegated key set so the distinct-on path routes under either binding

* fix(tests): run knowledge utils cases sequentially over shared per-test state
2026-07-22 13:15:15 -07:00
Waleed 936536e553 improvement(url-state): platform-wide nuqs audit — migrate remaining view-state, codify conventions (#5851)
* improvement(url-state): migrate ee settings sections to nuqs deep-linkable view-state

- audit-logs: types/time-range/start-date/end-date filters move to a co-located
  search-params.ts (reusing the logs kebab-token time-range parser); search binds
  to the shared settings ?search= via useSettingsSearch, replacing a hand-rolled
  debounce effect
- access-control: group detail deep-links via ?group-id (push/replace-on-close);
  search via useSettingsSearch
- custom-blocks: block detail deep-links via ?custom-block-id; create flow stays
  local; search via useSettingsSearch
- data-drains + forks: search via useSettingsSearch; forks close now replaces
  history like the mcp reference pattern
- polish: nullable-reason comment on logs startDate/endDate, stale debounce
  TSDoc now references useDebouncedSearchSetter

* fix(url-state): review fixes — resolve custom-block deep links before opening detail, per-surface time-range fallback

- custom-blocks: gate the detail view on the resolved block (matching mcp/
  access-control), so a dead or still-loading ?custom-block-id no longer flashes
  a bogus create screen
- parseAsTimeRange: unknown tokens now parse to null so each surface's
  .withDefault applies (logs keeps 'All time'; audit-logs keeps 'Past 30 days'
  instead of silently widening to all time on a malformed link)
- audit-logs: date-picker cancel target can never be 'Custom range' itself on a
  dateless custom deep link
- refresh shared ?search= consumer lists in TSDoc

* improvement(url-state): platform-wide sweep — migrate the last three view-state stragglers, codify conventions

Sweep across landing, workspace, and settings surfaces found only three
remaining candidates (everything else verified clean or correctly non-URL):

- knowledge document page: chunk enabled-status filter joins page/search/sort in
  the URL (?enabled=), resetting page in the same write
- workflow-mcp-servers: detail Details/Workflows tab deep-links via ?server-tab,
  cleared alongside the server id on close
- byok: provider search binds the shared settings ?search= via a controlled
  prop pair (modal/embedded consumers keep local state)

Rule updates (.claude/rules/sim-url-state.md): shallow defaults documented,
urlKeys kebab remapping, throttleMs deprecation, startTransition with
shallow:false, shared-parser null-fallback rule, resolve-before-open gating,
close-with-replace, and the reusable-component controlled-search pattern.

* improvement(url-state): cleanup pass — replace-on-close for the fork activity view, TSDoc form for the logs nullable comment

* fix(url-state): honor a custom time range only when both bounds are present

A partial ?time-range=custom deep link (missing start/end) now falls back to
the default preset window instead of displaying 'Custom range' while querying
an unbounded result set.

* fix(url-state): verification-round fixes — reject unparseable date params, tighten docs

- new parseAsDateString parser (logs + audit-logs): an unparseable
  ?start-date=/?end-date= now parses to null (missing bound) instead of
  crashing the audit-logs render via Invalid Date .toISOString(), and hardens
  the same class in logs
- audit-logs: remove the provably dead cancel-revert branch and its ref —
  the URL only holds 'Custom range' after Apply writes both bounds atomically
- workflow-mcp-servers: reset a lingering ?server-tab= when opening a server
  so a dead deep link can't re-target the next open
- knowledge document: drop the unreachable 'N selected' label branch
- sim-url-state.md fact-check corrections: cover apps/sim/ee in paths,
  focusedBlockId -> currentBlockId (the real store field), note that
  history/clearOnDefault are nuqs v2 defaults, fix the Suspense
  cross-reference and parseAsIsoDate serialize detail, clarify the *UrlKeys
  naming convention; list byok in the shared-search consumer docs

* fix(url-state): hold first paint while a custom-block deep link can still resolve

A valid ?custom-block-id= no longer flashes the list while the blocks query is
pending; a dead id still falls back to the list once loaded.

* fix(url-state): include permissions loading in the custom-block deep-link paint hold

canAdmin reads false while the permissions context loads, so the hold must
gate on permissionsLoading too; drop the blocksPending conjunct — it shares
one query with useCanPublishCustomBlock, so isLoading already covers it.
2026-07-22 12:16:31 -07:00
Theodore LiandClaude Fable 5 70814bc4a2 feat(db): role-keyed dbFor clients for cleanup and exec workloads (#5583)
* feat(db): role-keyed dbFor clients for cleanup and exec workloads

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNy9Fzpfc1FdHAzYyb6Ycy

* fix(db): keep cleanup-invoked helpers and snapshot reads on their role pools

Route markLargeValuesDeleted / pruneLargeValueMetadata (optional dbClient) and
chat-cleanup's file collection through the cleanup pool, and getSnapshot through
the exec pool, so the cleanup and inline-execution workloads stop borrowing the
process-wide pool for these queries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNy9Fzpfc1FdHAzYyb6Ycy

* fix(db): dbFor falls back to the process-role URL, not the base URL

With DATABASE_URL_WEB/TRIGGER set (as in prod) and the sub-pool URLs unset,
falling back to the base URL would silently shift execution-log and cleanup
traffic to a different PgBouncer endpoint on deploy. Chain the fallback
through the URL the process itself resolved so the rollout stays inert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNy9Fzpfc1FdHAzYyb6Ycy

* feat(db): log which connection each dbFor sub-pool resolved to

One line per role at first use: the dedicated DATABASE_URL_<ROLE> when set,
otherwise an explicit fallback message naming the process connection it
shares — so a missing/typo'd env var is visible at rollout instead of silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNy9Fzpfc1FdHAzYyb6Ycy

* feat(db): route pause/resume and large-value metadata persistence to the exec pool

Coverage scan follow-up: the paused_executions / resume_queue /
workflow_execution_logs transactions in human-in-the-loop-manager.ts and the
large-value owner/reference registration writes on the execution path now use
dbFor('exec'), matching the completion writes in the execution logger. All are
self-contained; billing calls remain outside the moved transactions on the
default client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNy9Fzpfc1FdHAzYyb6Ycy

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 14:35:42 -04:00
Waleed e841e4e731 fix(mcp): cap transport response bodies to bound a hostile tools/call payload (#5850)
* fix(mcp): cap transport response bodies to bound a hostile tools/call payload

A full-lifecycle comparison against LibreChat found the one place a reference
client was stricter: the live transport had no response-body byte cap, so a
hostile server could stream an unbounded tools/call result and OOM the process
(discovery and OAuth bodies were already capped). Cap non-GET response bodies at
16 MiB — counted as they stream, not buffered — while leaving the standalone GET
SSE notification stream uncapped (a cumulative cap would break its long-lived
nature). Mirrors LibreChat's transport response-size cap.

* fix(mcp): preserve url and redirected when capping a response body

new Response() resets url/redirected; the SDK resolves relative auth-metadata
URLs (resource_metadata) against response.url, so carry the originals over via
defineProperty on the wrapped response.

* fix(mcp): drop stale framing headers on a capped response body

The wrapped body is the already-decoded stream, so content-encoding/content-length
would misdescribe it — strip them, matching bufferUnderDeadline.
2026-07-22 10:48:10 -07:00
Waleed a0a437dc99 fix(deps): bump sharp to 0.35.3 and js-yaml to 4.3.0 for security advisories (#5848)
* fix(deps): bump sharp to 0.35.3 and js-yaml to 4.3.0 for security advisories

* chore: fix import order in file-reader
2026-07-22 10:26:10 -07:00
Waleed fd0d08a3b8 improvement(mcp): push-driven settings freshness + lifecycle-audit fixes (#5842)
* improvement(mcp): subscribe settings page to the live push, lean on it over re-probing

The list_changed → SSE push pipeline was already wired end-to-end but only
mounted in the workflow-editor tool picker. Subscribe the settings MCP page to
the same shared, reference-counted EventSource so tool changes reflect in real
time there too — the reference-client model (discover once, refresh on push).

With push now active on the settings page, raise MCP_SERVER_TOOLS_STALE_TIME
from 30s to 5min (matching the server-side cache TTL) so revisiting the page
leans on push + stored state instead of re-probing every connected server.
There is no background poll (no refetchInterval) — this only affects
refetch-on-visit-if-stale; real changes still arrive instantly via push.

* fix(mcp): re-sync tools on SSE reconnect so a missed list_changed can't strand stale tools

A dropped/reconnected EventSource may miss a tools_changed event during the gap,
which — with a longer stale time — could leave the page showing old tools until
a remount or manual refresh. Invalidate the workspace tools on reconnect (never
on the first open), the standard resync-on-reconnect pattern for push clients.

* fix(mcp): resync on re-subscribe so events missed while the tab was closed reconcile

Leaving the settings tab tears down the shared EventSource; remounting created a
new connection whose first open was skipped, so a tools_changed fired while
unsubscribed wasn't reconciled (the 5min stale time also won't refetch on
remount). Track a per-workspace 'ever subscribed' flag: skip resync only on the
session's first subscription (queries fetch fresh then); resync on the first
open of any re-subscription and on every reconnect.

* fix(mcp): reconcile a recovered server's status on successful discovery + fix stale comment

Lifecycle-audit findings:
- The per-server tools queryFn invalidated the server list only on error. A server
  that recovered via the stale re-probe returned fresh tools while the cached status
  still showed failed → 'tools present but row red' until the list independently
  refetched (a window the 5min stale-time widened). Now a successful probe against a
  server the cache still shows non-connected refreshes the list so the row clears.
- Correct the keep/drop comment: tools drop once the stored status leaves 'connected'
  (disconnected/error), not at a 3-failure threshold.

* fix(mcp): correct stale SSRF pin docstring and bound the remaining OAuth callback steps

Lifecycle-audit follow-ups (merged-code):
- validateMcpServerSsrf's return docstring described 'pin subsequent connections',
  which no longer holds for the public path — the returned IP is a policy signal
  selecting the validate-at-connect guarded fetch; redirect/rebind safety comes
  from per-connect validation + followRedirectsGuarded, not pinning (only the
  self-hosted private carve-out still literally pins). Corrected so a future
  reader can't reintroduce a real pin from the misleading text.
- The callback wrapped only the five post-auth steps in timedStep; the earlier
  loadOauthRowByState, getSession, and server SELECT (plus the provider_error
  clearState) were unbounded, contradicting the 'every awaited step bounded'
  invariant. Wrapped them so a wedged DB read surfaces as a labeled timeout.

* fix(mcp): resync on a first SSE open that recovered from an earlier connection error

If the initial EventSource connection errors before opening (and the initial tools
query fails with retry:false), the first successful onopen was skipped, leaving the
query stale. Track erroredBeforeOpen so a first open that followed a connection
error also resyncs — only a clean first subscription still skips.

* fix(mcp): make the SSE push subscription intrinsic to the tools query

The 5min stale time assumed push, but useMcpToolsEvents was only mounted in the
settings page and the tool picker — other consumers of the tools query (dynamic
args, tool selector, canvas block via useMcpToolsQuery/useMcpTools) had no
subscription, so list_changed updates could stay invisible for the full window.

Mount useMcpToolsEvents inside useMcpToolsQuery so every tools consumer gets
real-time push from the shared, reference-counted connection — no consumer is
left re-probing. Removed the now-redundant explicit mounts from the settings
page and tool-input.

* test(mcp): clear the shared SSE collections instead of reassigning globalThis

mcp.ts captures the connections Map and subscribed Set in module consts at import,
so setting the globalThis property to undefined didn't reset what the module uses —
subscription state leaked across tests. Clear the shared instances instead.

* fix(mcp): drop the success-path status reconciliation heuristic

The client-side serversList invalidation on a successful probe assumed the probe
updated the stored status, but a server-side cache hit returns tools without
touching status — so in the failed-cache-delete edge it fired a pointless
refetch. The benefit (instant vs the 60s serversList stale-time for status
recovery) doesn't justify the incorrect assumption; rely on the existing 60s
stale-time + SSE push for status recovery instead. Keeps the corrected keep/drop
comment.
2026-07-22 10:19:59 -07:00
Waleed e0e6f24f85 improvement(deployments): shorten lock windows and add tx safety timeouts on the deploy path (#5841) 2026-07-21 23:33:07 -07:00
Waleed c708add95f fix(mcp): keep last-known-good tools instead of flashing red on a transient failure (#5839)
* fix(mcp): keep last-known-good tools instead of flashing red on a transient failure

A connected server with cached tools was turning red 'Failed to discover' on a
single transient discovery probe blip, even though the stored status stayed
connected with tools — because useMcpToolsQuery dropped React Query's retained
data on error, and the row's showDiscoveryError fired over a populated server.

Now the aggregate keeps last-known-good tools across a failed refetch, and the
row only hard-reds when there are genuinely no tools to show. A persistent
failure still surfaces via the stored connectionStatus (gated by
MAX_CONSECUTIVE_FAILURES), matching how Claude Code / LibreChat / OpenCode /
VS Code handle a transient tools/list failure on a live connection.

Validated against those clients' source + the MCP spec before changing; this
supersedes the #5829 over-correction that showed the error even when tools
existed.

* fix(mcp): drop stale tools once a server is persistently failed, keep on transient

Sharpens the last-known-good behavior with the transient-vs-persistent distinction
the reference clients make: keep cached tools through a transient discovery failure
(stored status still healthy) so a populated server doesn't blank, but drop them
once the stored connectionStatus crosses its failure threshold to error/disconnected
— so the shared aggregate the workflow editor consumes stops offering a dead
server's stale tool schemas.
2026-07-21 23:29:54 -07:00
Waleed 1410aae3e3 fix(auth): gate email-otp auto-signup behind DISABLE_EMAIL_SIGNUP (#5840)
* fix(auth): gate email-otp auto-signup behind DISABLE_EMAIL_SIGNUP

* fix(auth): also close otp auto-signup under DISABLE_REGISTRATION
2026-07-21 23:26:44 -07:00
Waleed 93fbf584a2 feat(chat): soft-delete sidebar chats with restore from Recently Deleted (#5830)
* feat(chat): soft-delete sidebar chats with restore from Recently Deleted

* fix(chat): review round 1 — restore workspace authz, purge recheck, archived-list invalidation

* test(chat): update SSE handler assertions for workspaceLists invalidation

* fix(chat): bump updatedAt on restore, recheck retention cutoff in task cleanup

* fix(chat): drop explicit feedback delete in task cleanup — chat FK cascade covers it

* test(chat): cover restore route; guard legacy copilot delete from hard-deleting mothership chats

* chore: revert unintended bun.lock drift from worktree install

* fix(cleanup): recheck workflow archive cutoff on delete; export deletedAt in chat drain
2026-07-21 20:10:13 -07:00
Waleed 943d184365 fix(mcp): follow tools/list pagination instead of silently truncating (#5833)
* fix(mcp): follow tools/list pagination instead of silently truncating

The SDK's listTools() returns a single page; a server that paginates via
nextCursor was silently truncated to page one. Follow the cursor bounded by
four independent budgets (50 pages / 1000 tools / 5 MB / 60s aggregate
wall-clock) plus a repeated-cursor guard — a page cap alone can't stop a server
returning a fresh cursor with no new tools. Partial results from earlier pages
are kept when a later page fails; only a page-one failure throws. Matches the
LibreChat capped-cursor-loop pattern; caps live in MCP_CLIENT_CONSTANTS.

* fix(mcp): count UTF-8 bytes, keep empty partials, and log page-cap accurately

Review fixes on the tools/list pagination loop:
- Buffer.byteLength(..., 'utf8') instead of .length so non-ASCII schemas can't
  overshoot the 5 MB budget by counting UTF-16 code units.
- Track pagesFetched (not tools.length) for the partial-success decision, so a
  valid-but-empty first page followed by a failing page returns [] instead of
  throwing and marking the server unhealthy.
- Explicit reachedEnd/page-cap distinction so a natural finish on exactly
  MAX_PAGES isn't mislogged as truncated.
2026-07-21 19:32:57 -07:00
Vikhyath MondretiandSim Pi Agent 64353f210e feat(library): Best AI Agents for Data Extraction and RAG in 2026 (#5834)
* feat(library): Best AI Agents for Data Extraction and RAG in 2026

* fix(library): correct CSV import claims, fix comparison link, drop duplicate FAQ body

* fix(library): remove unsupported GDPR compliance claim

* improvement(library): update connector source count to match registry (50+)

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
2026-07-21 19:15:19 -07:00
Theodore Li 2cbecb5baa improvement(mothership): stable thinking indicator and jump-free streaming scroll (#5828)
* improvement(mothership): stable thinking indicator and jump-free streaming scroll

* fix(mothership): suppress shimmer over executing tool rows and seed chase interrupt baseline

* fix(mothership): keep shimmer mounted through the slot collapse so it animates out

* improvement(mothership): tighten transcript bottom padding, timed slot-exit latch, cleanup pass

* fix(mothership): bridge hidden special-tag streaming with the shimmer

* fix(mothership): hold sizer floor through reveal and reset chase deadline on park

* improvement(mothership): swap actions into the thinking slot at settle, quicken the chase
2026-07-21 22:11:40 -04:00
Vikhyath Mondreti 1b48eeb991 improvement(subblocks): trust block registry over stored subblock type (#5832) 2026-07-21 18:12:28 -07:00
Waleed a360cd4d7c fix(workflow): align condition/router handles with their rows (#5831) 2026-07-21 18:00:58 -07:00
Waleed 1f9f65df70 improvement(mcp): make the OAuth experience visible and verbally consistent (#5829)
* improvement(mcp): make the OAuth experience visible and verbally consistent

From a full UX-consistency audit of the MCP settings surfaces:
- One name for one action: 'Authorize' / 'Reopen authorization' everywhere
  (list chip, row, detail) — was three different labels across surfaces.
- The connecting state is now visible: the row subtitle shows a muted
  'Waiting for authorization...' instead of continuing to shout the red
  'OAuth authorization required' mid-flow.
- The Authorize affordance is a visible chip on the list row (was buried in
  the overflow menu), so a blocked popup's 'retry' has an obvious target.
- Removed the redundant aggregate discovery banner — every failing row
  already reports its specific error; two red messages for one failure.
- The header Refresh chip no longer renders an error sentence as its label
  (short 'Failed'; the Status field carries the explanation).
- Sentence-case sweep (Unnamed server, Not connected, Server name, Add MCP
  server / Edit MCP server, Add server, Test connection, Edit form, Delete
  MCP server), 'Search servers...' placeholder, row-subtitle/error text on
  the canonical tokens, and a Loading empty state instead of a blank flash.

* fix(mcp): show stale-discovery failures, best-effort popup-close label clear, drop redundant branch

- A failing latest discovery now shows its error even when cached tools exist
  (the stale tool count silently hid it).
- Best-effort popup.closed poll clears only the 'Waiting for authorization...'
  label share; the flow entry stays registered so a completion that still
  arrives over the BroadcastChannel is honored (settleFlow skips the
  double-decrement). Under COOP misreport the worst case is an early label
  reset, never a dropped completion.
- Remove the OAuth refresh-state branch made redundant by the 'Failed' change,
  plus its now-unused authType/error inputs.
2026-07-21 17:49:04 -07:00
Theodore Li e3f9deb65e fix(slack): allow empty status to clear the assistant status indicator (#5827) 2026-07-21 20:25:04 -04:00
Waleed e51a867de3 fix(mcp): open the OAuth popup synchronously and bound the start request (#5824)
* fix(mcp): open the OAuth popup synchronously and bound the start request

Two bugs behind 'pressed Connect/Reopen and nothing happened':
- window.open ran AFTER awaiting /oauth/start, outside the browser's user
  activation, so the popup was silently blocked (worst on the add-server flow).
  Popup-first now: open about:blank synchronously in the click (named window,
  so a re-click focuses/reuses an existing authorization window), navigate it
  once the start returns; close it on failure/already_authorized; clear toast
  when genuinely blocked (and skip the request entirely).
- /oauth/start had no client timeout, so a stalled start held the re-entrancy
  guard and the connecting label indefinitely. Bounded at 30s; on timeout the
  popup closes, the label resets, and retry is immediately available.

* fix(mcp): harden popup-first reopen edges

- Retire prior flows as soon as the named popup opens (the open already blanked
  any prior auth window; a failed start must not leave a windowless flow
  'connecting' for the 10-minute safety timeout).
- Check the COOP-fallback window.open result; when blocked, clear the state and
  toast instead of registering a windowless pending flow.
- Feature-detect AbortSignal.timeout (Safari <16) with an AbortController
  fallback for the bounded /oauth/start.

* fix(mcp): close the blank popup when post-start navigation fails

* chore(mcp): correct connecting-count ordering comment
2026-07-21 17:15:11 -07:00
Waleed 3458220ec9 fix(mcp): replace single-IP pinning with validate-at-connect SSRF guard (#5823)
* fix(mcp): replace single-IP pinning with validate-at-connect SSRF guard

Swaps the MCP transport + OAuth guard from pin-one-resolved-IP to the standard
pattern (LibreChat getSSRFConnect): DNS resolves normally and EVERY socket
connect filters the resolved addresses against the private/reserved blocklist,
closing the validate-then-trust window a rebind could race and removing the
non-standard mechanism implicated in the headers-then-no-body stalls (no
reference MCP client pins IPs; a pinned attempt welded to a bad flow cannot
escape, while retries rotate via resolver round-robin and succeed).

Adversarially reviewed before shipping; both findings closed here:
- Redirects are now followed manually with per-hop validation: an IP-literal
  redirect target (which bypasses ANY connect-time lookup - Node skips the
  custom lookup for numeric hosts) is checked explicitly, closing a metadata-
  endpoint redirect hole the old pin also had.
- Custom request headers are dropped on cross-origin hops, so a redirect to a
  second attacker host cannot harvest configured auth headers.

Policy gating unchanged: the guard activates exactly where the pin did
(validateMcpServerSsrf non-null; allowlist mode / localhost-on-self-hosted stay
unguarded). Non-MCP consumers of the pinned fetch are untouched. 397 tests
incl. new rebinding, mixed-answer, IP-literal-redirect, and hop-cap coverage.

* fix(mcp): restore IPv4 preference and harden the guarded redirect follower

- Restore validateUrlWithDNS's prefer-IPv4 resolution (a stale working-tree hunk
  accidentally reverted #5798 for the still-pinned non-MCP consumers).
- Validate the INITIAL url's IP-literal in followRedirectsGuarded, not just
  redirect hops, so the exported guard is self-contained.
- Drop entity headers (content-length/type/encoding) when a 301/302/303 switches
  a POST to a bodyless GET, which undici would otherwise reject.
- Lift method/headers/body/signal from a Request input instead of silently
  downgrading a guarded POST Request to a bare GET.

* fix(mcp): annotate headers double-cast and cancel redirect body before throw paths

- Add the missing double-cast-allowed annotation on the sanitized-headers cast
  (strict boundary audit).
- Cancel the redirect response body before the hop-cap / blocked-target throws
  so those paths can't leave a socket checked out on the long-lived Agent.

* fix(mcp): keep self-hosted private-resolving hosts unguarded (old-pin parity)

A DNS alias resolving to loopback/private is only reachable on self-hosted,
where the policy explicitly permits it; the guarded lookup would filter the
address and strand the connect where the old pin connected. Both MCP gates
(transport + OAuth guard) now route private/loopback resolutions over the
unguarded path, same as the localhost carve-out. Test IPs moved off RFC-5737
TEST-NET (which is correctly classified reserved).

* fix(mcp): pin (not skip) self-hosted private resolutions; refuse cross-origin body forwards

- The private/loopback carve-out now keeps the LEGACY PIN to the validated
  address instead of falling back to unguarded fetch — preserving both the old
  behavior and its anti-rebinding property for self-hosted DNS aliases.
- Cross-origin redirect hops now also refuse to forward a request body (307/308
  preserve method+body; post-pin those redirects really dial the new origin, so
  an open redirect could exfiltrate OAuth client secrets). Bodyless cross-origin
  redirects still follow. Tests for both.

* chore(mcp): true up stale pinned-era doc comments
2026-07-21 17:15:04 -07:00
Theodore Li e9898ea24d fix(ci): run Build App on the larger runner in GitHub mode (#5826) 2026-07-21 20:07:05 -04:00
Theodore Li 4b979780d2 fix(ci): make GitHub-hosted fallback actually build and test (#5814)
* fix(ci): make GitHub-hosted fallback actually build and test

* fix(ci): drop unused build-args passthrough so the heap ceiling can't be overridden

* chore(ci): trim comments to the non-obvious constraints

* fix(ci): build the app image on a larger runner in GitHub mode
2026-07-21 19:24:28 -04:00
Vikhyath Mondreti 995167dd18 improvement(admin): cleanup included usage settings for enterprise (#5825) 2026-07-21 16:22:43 -07:00
Waleed daa2416cba improvement(search-modal): remove matched-character bolding and move SearchHighlight into knowledge (#5822) 2026-07-21 15:39:56 -07:00
Waleed 6f96d4a3d4 fix(mcp): retire pooled connections after consecutive request timeouts (#5821)
* fix(mcp): retire pooled connections after consecutive request timeouts

Circuit breaker for the half-open-transport gap left by #5817: a lone timeout
still keeps the session warm (retiring on every timeout caused the
connect/stall/reconnect churn), but two consecutive timeouts with no healthy
request in between retire the connection so a genuinely half-open transport
can't serve repeated 30s failures until the liveness ping catches it. A healthy
release resets the count. Matches the LibreChat pattern of counting
connection-level failures rather than reacting to one.

* fix(mcp): classify TimeoutError-named aborts as timeouts for the circuit breaker

AbortSignal.timeout / undici surface a DOMException named TimeoutError whose
message lacks 'timed out'; without this the breaker treated it as a healthy
release and reset the streak. Adds name-based detection (incl. cause) + test.
2026-07-21 15:36:05 -07:00
Waleed 9fb9ac54e4 diag(mcp): per-request phase logging on the streamable-HTTP transport (#5819)
* diag(mcp): per-request phase logging on the streamable-HTTP transport

Mirrors the OAuth guarded-fetch logging that isolated the /oauth/start body
stall. Logs each transport request (host+method), the response headers with
TTFB, and failures — so a first-connect stall shows whether it hangs before
response headers (connect/request) or after (the SDK's stream read), which
isolates the client-side first-connect stall we haven't yet root-caused.

* diag(mcp): include error name/code in transport failure log

Distinguishes abort vs connect-refused vs TLS vs timeout in the failure-phase
data.
2026-07-21 15:10:27 -07:00
Waleed 051c86f696 fix(mcp): keep the pooled connection warm on a request timeout (#5817)
Empirically: our client's first connect to these servers consistently hangs to
our 30s timeout, then a retry connects in ~100ms (the server answers initialize
in ~120ms from a normal client — proven via A/B). Treating that request timeout
as a dead connection (isDeadConnectionError) retired the pooled connection, so
the next discovery reconnected and paid the stall again — a connect/stall/
reconnect churn loop, and the cause of the intermittent 'Failed to discover' /
disconnected states.

A streamable-HTTP request timeout aborts only that request's own POST stream;
the session stays healthy for the next request. So stop poisoning the lease on a
timeout (matches the MCP SDK, OpenCode, and LibreChat, none of which retire a
connection on a request timeout). Only a real transport close/404/reset retires.
2026-07-21 15:10:21 -07:00
Theodore LiandClaude Fable 5 bcad4e308c fix(pii): mask offloaded large payloads chunk-by-chunk instead of aborting at 16MB (#5810)
* fix(pii): mask offloaded large payloads chunk-by-chunk and retry transient mask failures

A block output past the 16MB inline materialization ceiling aborted the run
before masking even started: the redaction path hydrated the whole offloaded
value at once, and the pre-flight size assert fired on the manifest's total
byteSize. Large-array manifests now page one stored chunk at a time
(materialize -> mask -> re-store, rebuilt via the manifest writer with preview
derived from masked items), so peak heap stays ~one chunk regardless of payload
size. Single refs up to the 64MB durable cap hydrate with a raised budget and
run serially outside the concurrency pool.

Mask-batch chunk requests now retry transient failures (network errors,
408/429/5xx, honoring Retry-After) with jittered backoff, so a single ALB blip
or Presidio pod restart no longer fails a whole payload's redaction. Nested-ref
masking now runs the string pass before ref substitution, fixing a latent
double-mask when a masked nested value shrinks back inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): retry runtime timeouts and socket closes in mask-batch chunks

Verified end-to-end against a 26MB / 40k-record offloaded output: the
chunk-wise path masks it in ~54s on a single local Presidio worker where the
old path aborted at the 16MB ceiling. The exercise surfaced two more transient
error shapes the retry classifier missed — runtime-level request timeouts
(undici's default 300s headers timeout, Bun's TimeoutError) and mid-flight
socket closes — both of which previously failed the whole payload's redaction
on the first occurrence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): gate the spaCy fast path on entities the loaded models can produce

The registry's SpacyRecognizers claim every entity in Presidio's default
NER-model mapping — including PHONE_NUMBER/AGE/ID/EMAIL, which exist for
transformer de-identification backends and which no spaCy model can emit. The
NER_ENTITIES derivation trusted that claim, so any request naming PHONE_NUMBER
(present in nearly every redaction rule) silently forced the full spaCy pass
and the regex-only fast path never fired. Intersect the claimed set with the
entities the loaded models' actual NER labels map onto; the hard floor of core
NER entities is unchanged, and a future backend that genuinely emits phone
labels would re-gate automatically.

Verified live: PHONE_NUMBER-only requests take nlp=skip with span parity
against the full path, PERSON still forces NER, and a 26MB/40k-record
block-output redaction with the realistic entity set runs entirely on the
fast path (~3.2min vs ~15min projected full-NER on one worker).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): hydrate oversized-chunk manifests serially, not just single refs

Review finding: a manifest whose packer emitted a chunk past the inline
ceiling (one item larger than the chunk target) hydrates that chunk with the
raised 64MB budget inside the REF_CONCURRENCY pool, so several such manifests
could hydrate oversized blobs concurrently — the exact heap scenario the
serial path exists to prevent. The serial gate now covers any ref whose
hydration can exceed the inline ceiling: oversized single refs and manifests
containing an oversized chunk. Normally-chunked manifests stay pooled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): serialize oversized hydrations globally across nested redaction passes

Review finding: the serial gate was per-resolveReplacements invocation, so
nested oversized refs discovered inside different pooled parents each got
their own pool and could hydrate oversized blobs concurrently. A shared
promise-chain gate now threads through the options from the entry points, and
a reentrancy flag lets a gated ref's own nested oversized work run directly
instead of deadlocking on the hold. Covered by a cross-parent max-in-flight
assertion and a nested-oversized deadlock regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): fail fast on a null mask-batch body; use sleep() in gate test

A 200 response with a null JSON body threw TypeError on the data.masked read,
which the retry classifier treats as transient — burning the full retry budget
on a deterministic shape failure. Null-guard the body so it throws the
non-retryable shape error immediately. Also swap the gate test's inline
setTimeout promise for sleep() to satisfy check:utils, which failed CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:03:14 -04:00
Waleed 7ee8f46a70 feat(access-control): per-group chat-deploy auth modes + polish (#5818)
* feat(access-control): per-group chat-deploy auth modes + polish

- Add a per-permission-group 'Auth modes chat deployments may use'
  control, mirroring the existing public-file-share auth-mode control,
  so admins can disable specific chat auth modes (Public, Password, ...)
- Enforce it server-side on chat create/update via validateChatDeployAuth
  (ChatDeployAuthNotAllowedError -> 403), and filter the chat deploy UI's
  Access-control options to the allowed set (keeping the saved mode visible)
- Remove the dead 'Template' deploy option (hideDeployTemplate had no consumer)
- Shrink access-control block permission icons (size-[9px]) so they no
  longer touch their tile borders; normalize the tiles to size-[16px]

* fix(access-control): gate copilot chat deploy + fix new-chat default auth

- Enforce validateChatDeployAuth on the copilot executeDeployChat path
  (it called performChatDeploy directly, bypassing the HTTP-route gate)
- Enforce on update only when the auth mode actually changes, so a
  grandfathered saved mode can be re-saved (e.g. title-only edits)
- Chat deploy UI now grandfathers only the saved mode, not the unsaved
  'public' default; snap a new chat off a disallowed default to the first
  allowed mode so it can't submit a value the server rejects

* fix(access-control): keep a saved SSO chat mode selectable when SSO env is off

Treat savedAuthType === 'sso' as SSO-enabled when building auth options so an
existing SSO chat isn't dropped and silently downgraded by the snap effect.
Mirrors the file-share modal.

* improvement(access-control): require at least one auth mode when enabled

Guard the chat-deploy and file-share auth-mode multiselects so the last
allowed mode can't be removed while the feature is enabled — an empty
allow-list would silently block every deployment/share. Disabling the
feature entirely stays the job of the Hide Chat toggle / Public Sharing
checkbox.

* fix(access-control): never leave a new chat with zero selectable auth modes

Surface SSO in the chat deploy options whenever the permission group
explicitly allows it (not only when the env flag is on or the chat is
saved as SSO), so a group whose only allowed mode is SSO can't produce an
empty option set that strands a new chat on a disallowed default. Also
drop a redundant trailing comment in the copilot deploy handler.
2026-07-21 14:46:54 -07:00
Theodore Li 407c7297c1 fix(custom-blocks): render and execute custom blocks as agent tools (#5811)
* fix(custom-blocks): render and execute custom blocks as agent tools

* fix(custom-blocks): address review findings on agent-tool execution

* fix(custom-blocks): move executor tool id out of the custom-tool namespace
2026-07-21 17:37:15 -04:00
Vikhyath Mondreti 37628f1566 fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant (#5790)
* fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant

* improvement(sockets): state improvements

* address comments
2026-07-21 14:36:11 -07:00
Waleed d35bc77053 fix(mcp): don't blank the MCP tools page when tool discovery fails (#5816)
* fix(mcp): don't blank the MCP tools page when tool discovery fails

A tool-discovery error (one slow/failing server, e.g. a stalled transport)
replaced the entire server list with an error banner. Gate the full-list
replacement on serversError (the list genuinely failing to load) only; when the
servers loaded, always render the list — each row already surfaces its own
discovery state via toolsStateByServer, with a non-blocking notice above the
list. Restores graceful degradation so one bad server can't hide the others.

* fix(mcp): surface partial discovery failures per-row and in the notice

When one server succeeds and another fails, the aggregate toolsError is
suppressed (data exists), so the failure was hidden. Now: the notice renders on
ANY per-server discovery error (not just all-fail), and each failed row surfaces
its live discovery error instead of reading as '0 tools' before its stored
status catches up.
2026-07-21 14:06:23 -07:00
Waleed 94b23344e6 improvement(emcn): consistent hugging tab bars via ChipModalTabs (#5815)
- Pin ChipModalTabs to w-fit so the segmented pill always hugs its tabs
  instead of stretching full-width when dropped into a flex column
- Migrate the mothership settings tab bar off a hand-rolled full-width
  underline bar onto ChipModalTabs
- Fix undefined --border-secondary token (fell back to currentColor) in
  mothership -> --border, matching the admin settings precedent
2026-07-21 13:28:42 -07:00
Waleed 108fea13cd feat(providers): add Gemini 3.6 Flash and 3.5 Flash-Lite (#5812) 2026-07-21 13:21:26 -07:00