Sitelet https://github.com/simstudioai/sim/compare/main...staging
Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: simstudioai/sim
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: simstudioai/sim
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: staging
Choose a head ref
Checking mergeability… Don’t worry, you can still create the pull request.
  • 20 commits
  • 876 files changed
  • 5 contributors

Commits on Aug 25, 2026

  1. fix(memory): close app-service leak paths behind the per-task memory …

    …ramp (#7056)
    
    * fix(memory): close app-service leak paths behind the per-task memory ramp
    
    Prod app tasks climb from ~3.0 GB to 9+ GB average (20.6 GB worst task)
    with uptime and reset only on deploy. The growth lives in the main Next.js
    process — the isolated-vm worker is a separate, bounded child and its
    disposal already runs in finally on every path. Four fixes at the sites
    that can actually accumulate there:
    
    - copilot stream teardown (lib/copilot/request/lifecycle/start.ts): the
      activeStreams registration, the 250ms Redis abort poller, and the SSE
      keepalive were acquired outside the try whose finally releases them, so
      a throw before the lifecycle started (e.g. resetBuffer on a Redis blip)
      or a throw inside the ordered teardown orphaned two immortal intervals
      and the registration. Add an idempotent backstop in the orchestration's
      outer finally; the ordered teardown sets a flag so the normal path pays
      nothing.
    
    - large-value cache (lib/execution/payloads/cache.ts): expiry was enforced
      only inside later cache calls, so on a quieting instance the last
      entries — parsed object graphs worth a multiple of their JSON-byte
      accounting — sat indefinitely instead of for the 15-minute TTL. Add a
      self-retiring, unref'd sweep interval, and export occupancy stats.
    
    - memory telemetry (lib/monitoring/memory-telemetry.ts): add the
      large-value cache occupancy and detached-context count to the periodic
      snapshot so the JSON-bytes-vs-heap amplification and context retention
      are readable from the same log line as heapUsedMB.
    
    - BYOK rotation cursors (lib/api-key/byok.ts): the tenant-keyed cursor Map
      had no delete, TTL, or ceiling. Bound it with an LRU; evicting an idle
      pool's cursor just restarts its rotation at index 0.
    
    - collab-doc converter (lib/collab-doc/converter.ts): the DOM guard read
      the bundled module's `window` binding while the install wrote
      `globalThis.window` — the same bundler mismatch documented for TipTap in
      next.config.ts — so a runtime where the two disagree re-allocated a
      multi-MB jsdom window on every conversion. Guard and install now go
      through globalThis, and the jsdom window is a module singleton either
      way.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
    
    * improvement(execution): idle-TTL touch-on-read + LRU eviction for the large-value cache
    
    Entry lifetimes were absolute-from-insert and eviction order was
    insertion order, so a value a live run kept referencing could expire or
    be pressure-evicted mid-use — while a genuinely idle entry survived the
    full window. Every authorized read now refreshes the expiry and moves
    the entry to the back of the eviction order: expiry and eviction only
    ever take entries nothing has read for a full TTL, and pressure eviction
    takes the least-recently-used recoverable entry. Strictly fewer
    mid-execution misses; TTL values, the admission budget, and the
    sole-copy (non-recoverable) eviction protection are unchanged.
    
    Touching stays behind the scope check so an unauthorized probe cannot
    extend a lifetime. Module TSDoc now records the standing constraint:
    the warm pass runs once per execution start, so the TTL must outlive
    the warm-to-first-reference gap — do not shorten it until warming is
    per-block.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
    icecrasher321 and claude authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    b7b0be8 View commit details
    Browse the repository at this point in the history
  2. fix(ui): unify branded error pages (#7057)

    * fix(ui): unify branded error pages
    
    * fix(desktop): match canonical chip chrome
    
    * test(ui): update error chip mock
    
    * fix(desktop): package offline font reliably
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    49a3399 View commit details
    Browse the repository at this point in the history
  3. fix(sse): bound workspace SSE connection lifetime (#7058)

    * fix(sse): bound workspace SSE connection lifetime
    
    Teardown ran only from the request abort listener and the stream cancel callback, both of which fire only when the runtime reports a client disconnect. Nothing else bounded the connection, so a missed report left the pub/sub handler, the heartbeat timer, and the stream's undrained queue held for the life of the process.
    
    Add a jittered lifetime ceiling checked on the existing heartbeat tick, tighten reclaim for a vanished consumer via desiredSize, remove the abort listener on every teardown path, and run full teardown when a heartbeat enqueue fails. Log the close reason so opens minus closes is observable.
    
    * fix(mothership): resync chat caches after an SSE reconnect gap
    
    task_status events are transient and never replayed, so any window with no open connection can drop a create, rename, delete, or completion. The chat hook reconnected silently and reconciled nothing, leaving list and detail caches stale until an unrelated action refreshed them.
    
    Resync on reconnect, on the first open of a re-subscription, and on a first open that only succeeded after an error, matching the pattern useMcpToolsEvents already uses for the same gap.
    
    * fix(mothership): keep reconnect resync off locally streaming chats
    
    The resync invalidated every chat detail, including one whose stream this client is rendering optimistically. Refetching there replaces the local transcript with a server copy that does not yet hold the in-flight message, which is exactly what status events avoid via shouldSkipDetailInvalidationForStreamEvent.
    
    Filter the detail invalidation with the same isLocalOptimisticActiveStream check. Those chats reconcile when their own stream finishes.
    
    * fix(mothership): only skip resync for a stream still running
    
    Optimistic markers alone were the skip condition, but a finished turn can leave activeStreamId and its live-assistant message cached when finalization skips detail invalidation for a queued follow-up. That chat would then be excluded from every future resync — permanently, since only a refetch clears the markers, and the resync was the refetch.
    
    Gate the skip on a non-terminal streamSnapshot status so it covers turns that are genuinely still streaming. Exports isTerminalStreamStatus, which was already the private check for this in effective-transcript.
    
    * fix(sse): raise the ceiling and narrow reconnect resync to the lists
    
    Deciding from cache whether a chat is still streaming is not reliable — the optimistic markers outlive the turn, and each refinement of that predicate exposed another state where it answers wrongly. Drop it: the resync now invalidates only the workspace lists, which is always safe, and chat detail reconciliation stays as it is today rather than being half-solved here.
    
    Raise the ceiling to 4h, matching lib/realtime/event-stream-route.ts. A healthy client is drained and so is never unread; the unread check is what reclaims a vanished consumer, and it does so within minutes. A short ceiling would therefore only force reconnects on the connections that are working, and every reconnect is a window where a transient event can be missed. Retention stays bounded by the ceiling instead of by process uptime.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    ef42424 View commit details
    Browse the repository at this point in the history
  4. Configuration menu
    Copy the full SHA
    1e24a6a View commit details
    Browse the repository at this point in the history
  5. fix(sse): rotate workspace streams without gaps (#7061)

    * fix(sse): rotate workspace streams without gaps
    
    * test(sse): cover delivery across rotation
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    ea9207d View commit details
    Browse the repository at this point in the history
  6. refactor: remove dead orchestration entry points and no-op tests (#7060)

    * refactor(orchestration): remove seven unreferenced perform* entry points
    
    Each had exactly one declaration, a barrel re-export, and no caller anywhere
    in the repo — no route, no application use case, no tool handler, no test.
    Their Params/Result interfaces went with them where nothing else consumed
    them; PerformCredentialResult, PerformUpdateWorkflowParams and
    PerformUpdateWorkflowResult stay, since live functions still use them.
    
    Removed: performDeleteCredential, performGetWorkspaceFileShare,
    performUpsertWorkspaceFileShare, performMoveRenameWorkspaceFile,
    performUpdateTableDescription, performUpdateWorkflow,
    performUpdateWorkspaceFileContent.
    
    * test: drop assertions that cannot fail
    
    Four tests asserted nothing about the code under test:
    
    - app/api/copilot/methods/route.test.ts was the directory's only file — it
      asserted expect(true).toBe(true) against a route that does not exist.
    - tools/index.test.ts carried a block self-documented as existing "to
      maintain test count".
    - mcp/storage/memory-cache.test.ts closed a delete-a-missing-key case with
      expect(true).toBe(true); it now asserts the call resolves without throwing.
    - realtime/src/index.test.ts checked typeof roomManager.x === 'function' and
      typeof process.on === 'function', both of which tsc already proves.
    
    Removing the realtime cases leaves a real gap: index.ts registers
    uncaughtException, unhandledRejection, SIGINT and SIGTERM handlers with no
    coverage. Better to have that gap visible than papered over by a test that
    would pass with the handlers deleted.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    167fcb4 View commit details
    Browse the repository at this point in the history
  7. refactor: consolidate three drifted copies, close a gate blind spot, …

    …fix stale docs (#7062)
    
    * refactor(workflows): one owner for new-workflow sort order
    
    The same ~35-line query — parent condition for workflows and folders, two
    parallel min(sortOrder) reads, fold to a min, subtract one, fall back to 0 —
    existed three times:
    
      lib/workflows/utils.ts               inline in createWorkflowRecord
      lib/workflows/orchestration/...      as a file-private nextWorkflowSortOrder
      lib/workflows/persistence/duplicate  inline, inside the duplicate transaction
    
    The first two are character-identical modulo the table alias. The third had
    drifted: it omits isNull(workflow.archivedAt), which the other two apply, so a
    folder whose lowest-sortOrder workflow is soft-deleted positioned a *duplicate*
    differently from a *create*. The folder-side query agrees in all three, which
    marks it as a copy-paste slip rather than intent.
    
    Promotes the helper to lib/workflows/sort-order.ts, taking an optional DbOrTx
    so the duplicate path can keep reading inside its transaction. Its own module
    rather than utils.ts because duplicate.test.ts and workflow-lifecycle.test.ts
    both mock '@/lib/workflows/utils' wholesale — from a separate module the real
    query still runs under those suites, so their existing sort-order assertions
    keep their meaning and needed no edits.
    
    Note the archived-row behavior itself is not unit-testable here: the shared
    dbChainMock does not evaluate WHERE predicates. The guarantee is structural —
    one query builder instead of three means the predicate can no longer drift.
    
    * fix(ee): case-fold the stored integration allowlist
    
    ee/access-control re-implemented the allowlist intersection instead of calling
    intersectIntegrationAllowlists, and lost the case-folding: normalization only
    happened on the envAllowlist !== null branch, so with ALLOWED_INTEGRATIONS
    unset a stored config went through untouched. Callers compare against
    blockType.toLowerCase(), so a stored 'Slack' failed to match 'slack' and the
    block was denied.
    
    The access-control UI writes block.type directly and block types are lowercase,
    so this is not reachable from the UI — but allowedIntegrations is a bare
    z.array(z.string()) on the wire, so any API client can store mixed case.
    
    Replaces the fork with the shared helper. Adds two tests; the first fails
    against the old code.
    
    * fix(queries): forward the abort signal to getFullOrganization
    
    useOrganization destructured `signal` from the queryFn and passed it to
    fetchOrganization, which named the parameter `_signal` and never used it — so
    the org detail fetch could not be cancelled. Switching orgs rapidly left every
    prior request in flight, free to resolve out of order.
    
    Better Auth takes cancellation two ways and both are already used here:
    fetchOptions on the params object (session.ts:21) and a second argument
    (admin-users.ts:129). Uses the former. This was the only `_signal` under
    apps/sim/hooks.
    
    The existing transition test asserted the exact call shape, so it now asserts
    intent via objectContaining. Adds a test for the signal itself; it fails
    against the old code.
    
    * fix(canvas): select from useWorkflowRegistry instead of subscribing whole
    
    check-zustand-v5-selectors matched /use[A-Z]\w*Store\(/, and exactly one
    Zustand store in the repo is not named with a Store suffix —
    useWorkflowRegistry. So the store behind the canvas went unchecked, and two
    bare whole-store subscriptions had accumulated in the action bar, re-rendering
    it on every registry mutation (clipboard, hydration, pendingSelection,
    activeWorkflowId). Every other call site in the repo already uses a selector.
    
    Widens the pattern to (?:Store|Registry) and fixes both call sites. The
    widened gate reports these two and nothing else, so there is no cleanup tail.
    
    * docs: correct comments that name symbols which no longer exist
    
    Each of these points a reader at an identifier that is not in the repo:
    
    - table/import-data.ts, table/service.ts — `acquireTablePositionLock` and
      `nextAutoPosition` were removed with the service.ts split; the surviving lock
      is `acquireRowOrderLock`, which import-data.ts already imports and calls.
      service.ts's mention is load-bearing: it exists to tell the reader which
      other lock this one mirrors, for lock-ordering.
    - resources/orchestration/restore-resource.ts — named `performRestoreFolder`
      (the callee is `restoreFolder`) and described a `'workflow'` default it
      falls back to. There is no such default: resourceType is required and the
      config lookup is a bare index. Describing a fiction is how a future reader
      talks themselves into relaxing the total Record to a Partial.
    - knowledge/search/queries.ts — cited apps/docs/app/api/chat/route.ts, deleted
      with Ask AI. The k=60 it pins against now lives in the docs search route.
    - rate-limiter/hosted-key/queue.ts — documented a `waitForHead` method the
      class does not have; the queue exposes `checkHead` and the polling loop is
      private to the consumer.
    - logs/log-views.ts — a "Level 1.5 / 2 / 3" scheme that appears nowhere else;
      the real contract is the five named views. Dropped, and the three banner
      rules with it (CLAUDE.md bans banner separators).
    
    Comment-only apart from the log-views banners.
    
    * refactor(ui): derive three values instead of storing or memoizing them
    
    - import-modal: browserId and profileId were state corrected by two effects
      when a reload dropped the selection. That commits and paints one frame in
      which the profile still belongs to the previously selected browser — and
      Import is enabled during it, submitting via a `profiles.find` that searches
      every browser's profiles. Both now fall back during render, and `selected`
      searches only the current browser's profiles. Covered by the existing
      'never leaves a profile selected that belongs to another browser' test.
    
    - workflow.tsx: isWorkflowEmpty was a second useMemo over the same [blocks]
      dep computing exactly !hasBlocks, allocating its own Object.keys array.
      Both feed primitives, so neither memo bought identity stability.
    
    - thinking-loader: an effect seeding cycleVariant whenever variant is defined,
      which `shown = variant ?? cycleVariant` can never read. On the one
      transition where cycleVariant becomes visible (variant going undefined) the
      cycling effect assigns it in every branch — settle, reduced-motion, and
      tick — in the same flush, so the seed was never observable.
    
    * fix(review): exclude deleted folders from sort order; make the allowlist tests real
    
    Three findings from cubic on #7062, all valid.
    
    1. nextWorkflowSortOrder consulted the folder minimum without excluding
       soft-deleted folders. Because the helper returns min - 1, a deleted folder
       holding the lowest slot ratchets the floor down permanently — the same class
       of bug as the archived-workflow one this PR set out to fix, on the other half
       of the query. lib/folders/orchestration.ts already documents this exact
       rationale for the folder-creation side of the same algorithm, and the uploads
       folder manager filters it too; this was the outlier.
    
    2. The two new allowlist tests did not call setEnterpriseOrgWorkspace(), so
       resolution never reached the group queries and validateBlockType returned
       early. They passed against the unfixed code when run in isolation and only
       appeared to fail in a full-file run, where mock state leaked from earlier
       tests. Verified with 'vitest -t': both now fail without the case-folding fix
       and pass with it.
    
    3. The restore-resource comment this PR rewrote was itself wrong. A Partial map
       does not defer the failure into the cascade — the lookup widens to
       FolderResourceType | undefined and the error lands on the restoreFolder call
       site. Reworded to say that, and why keeping the check at the mapping matters.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    bdda083 View commit details
    Browse the repository at this point in the history
  8. refactor: remove dead code, orphan modules, and two unused dependenci…

    …es (#7063)
    
    * refactor: remove dead code, orphan modules, and two unused dependencies
    
    Every symbol below was classified per reference hit as: own declaration /
    barrel re-export line / vi.mock stub / real production caller. Only symbols
    with zero real callers are removed.
    
    Orphan modules (nothing imports them):
    - scheduled-tasks/components/schedule-calendar/ — 11 files. Zero references
      repo-wide, not even a barrel line.
    - terminal/components/filter-popover/ — re-exported through a barrel,
      consumed by nothing.
    - 6 iso-marks primitives (IsoCubeGrid, IsoCubeRow, IsoFourBox, IsoGridPlane,
      IsoStackedPlanes, IsoStar). mothership.tsx imports only the four
      Iso*Illustrations, which do not use these.
    - 4 copilot user-input hooks (useMentionKeyboard, useTextareaAutoResize,
      useMentionInsertHandlers, useCaretViewport) — barrel line only. Their six
      siblings in that directory are live and stay.
    - lib/workflows/application/duplicate-workflow.ts — an orphaned authorized use
      case. Live duplication goes through lib/workflows/persistence/duplicate.ts,
      which exports a same-named duplicateWorkflow; the 23 apparent references are
      all to that one. Nothing imports the application path.
    
    Dead exports:
    - lib/compare/data/feature-catalog.ts (SIM_FEATURES, 939 lines) plus the
      SimFeature type, FeatureCategory union, and featuresByCategory/featuresByTag
      helpers that only it used — one unshipped feature-comparison catalog.
    - createExecutionCallbacks (execution-events.ts) — declaration only, zero
      other hits repo-wide.
    
    Stale vi.mock targets — mocks whose module no longer exists, so they are
    silent no-ops that make a test look protected when it is not:
    - @/background/logs-webhook-delivery and @/app/api/webhooks/utils in the
      webhook trigger route test (its generateRequestHashMock and
      validateSlackSignatureMock hoisted vars went with it)
    - @/lib/workflows/subblocks/options in blocks/blocks/logs.test.ts
    - @/lib/uploads/setup in the S3 client test
    - @/lib/uploads/setup.server in the files delete and parse route tests —
      parse/route.test.ts mocks the real @/lib/uploads/core/setup.server on the
      very next line
    
    Dependencies: three and @types/three. Zero imports anywhere, and absent from
    every config, script, workflow, Dockerfile and chart. Both arrived with the
    speech-to-speech voice mode, which has since been removed.
    
    Deliberately kept: build-chat-animation/ reads as an orphan but
    build-callout.tsx documents it as parked unwired for reuse.
    
    * chore: update lockfile for the three/@types/three removal
    
    CI runs bun install --frozen-lockfile, so dropping the two dependencies from
    apps/sim/package.json without regenerating bun.lock fails the install step.
    
    The diff is the two entries plus three's transitives (@dimforge/rapier3d-compat,
    @tweenjs/tween.js, @types/stats.js, @types/webxr, @webgpu/types, meshoptimizer).
    
    One change deserves a note: top-level fflate moves 0.8.3 -> 0.4.8. It was
    hoisted to 0.8.3 only because @types/three required ~0.8.2; with that gone the
    remaining top-level consumer is posthog-js, which asks for ^0.4.8. Every other
    consumer keeps its own pinned nested entry (@shuding/opentype.js 0.7.4,
    @smithy/middleware-compression 0.8.1), and no source file imports fflate
    directly, so no resolution changes for anything that was already installed.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    011f26d View commit details
    Browse the repository at this point in the history
  9. improvement(tools): support duplicate provider instances (#7064)

    * improvement(tools): support duplicate provider instances
    
    * fix(tools): cover duplicate Pi tool instances
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    174c773 View commit details
    Browse the repository at this point in the history
  10. feat(library): Best Multi-Agent Frameworks for Production in 2026 (#7065

    )
    
    Co-authored-by: Sim Pi Agent <pi@sim.ai>
    icecrasher321 and Sim Pi Agent authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    1e1298b View commit details
    Browse the repository at this point in the history
  11. feat(api): make the platform operable headless over v2 (#6912)

    * feat(v2): download run output files by API key
    
    Adds GET /api/v2/workflows/{id}/runs/{runId}/files/{fileId}, closing the
    async-run loop for headless callers. A run's output carries UserFile URLs
    pointing at /api/files/serve/..., which rejects x-api-key outright, so an
    async run that produces a file previously had no byte path out for an API
    key at all.
    
    The file is addressed by the id the run reported and resolved against the
    run's own recorded execution data, from which the storage key is read. The
    request never supplies a storage key, so the endpoint cannot be aimed at
    bytes the run did not produce. Resolution deliberately reads the
    materialized-but-undisplayed recording, because the display projection
    strips exactly the `key`/`context` fields a byte read needs.
    
    Also hardens normalizeStartFile to derive a file's storage key only from a
    validated internal serve URL, discarding any caller-supplied `key`/`context`.
    A workspace API key has no human subject, so the executor resolves its actor
    to the workspace billing owner (preprocessing.ts -> resolveSystemBillingAttribution);
    verifyFileAccess then authorizes a workspace-context key as that owner, whose
    reach is not bounded by the key's workspace. Accepting an attacker-authorable
    key made that substitution exploitable as a confused deputy. Normalization is
    all-or-nothing, so a forged file now drops the whole files input.
    
    * feat(workflows): one graph-write door, principal-derived audit source, and v2 authoring endpoints
    
    Extract replaceWorkflowNormalizedState as the single persistence primitive for a
    workflow graph replace and route both the internal editor save and the Copilot
    edit tool through it, so neither can skip state preparation, the row lock, the
    lastSynced stamp, or custom-tool extraction by choosing a different entry point.
    
    Derive the audit source from the acting principal instead of hardcoding
    'copilot', then widen workflows.variables.apply_operations and
    workflows.bulk.move to every principal kind.
    
    Add GET/PUT /api/v2/workflows/{id}/state, POST /operations, /duplicate,
    /restore, PATCH /variables, and POST /api/v2/workflows/move over surface-neutral
    application use cases; move the edit engine to lib/workflows/editing.
    
    * test(workflows): cover the graph-write primitive, audit source, and the v2 authoring surface
    
    Pin the two-doors fix (preparation runs, the row is locked, custom-tool
    extraction is post-commit and best-effort) and the false-audit fix (a session
    principal writes source: 'session', a delegated one writes its service). Both
    were verified to fail with the fix reverted.
    
    Add the application matrix for replaceWorkflowState, applyWorkflowOperations,
    readWorkflowGraph, and restoreWorkflow — role floor, principal-kind rejection
    before canonical load, asserted-scope concealment, lock, validation, atomic
    conflict, plan gate, and audit-then-notify ordering — plus route tests for
    every new endpoint.
    
    * test(workflows): pin the internal graph-write door and the v2 list scope
    
    Characterize saveWorkflowNormalizedState's statuses, messages, and notification
    after the persistence extraction, and cover the new scope filter on
    GET /api/v2/workflows including a cursor replayed under a different scope.
    
    * feat(api): add v2 block, tool, connector-type, and enrichment catalogs
    
    Adds six read endpoints under /api/v2 that publish Sim's code-defined
    catalogs: GET /blocks, GET /blocks/{blockId}, GET /tools,
    GET /tools/{toolId}, GET /connector-types, and GET /enrichments.
    
    These read like static reference data and are not. What a caller may
    place is decided per workspace by its permission-group integration
    allowlist, per organization by which unreleased blocks have been
    revealed, per deployment by ALLOWED_INTEGRATIONS, and per workspace
    again by the workflows it has deployed as blocks. So all six are plain
    defineWorkspaceOperation reads at minimumRole 'read' with
    workspaceApiKey 'allow' — the exact policy of credentials.providers.list
    — and every response keeps Cache-Control: private, no-store, because an
    unrevealed preview block's existence must not leak across organizations
    through a shared cache.
    
    Trigger blocks ride as ?capability=trigger rather than a second
    endpoint, and workspace custom blocks ride inside /blocks discriminated
    by `source`, so "what may I place?" stays a one-call question.
    
    The block projection is extracted out of the Copilot get_blocks_metadata
    tool and rewritten onto @/tools/metadata and @/tools/metadata-outputs.
    That cuts the tool's own @/tools/registry edge as a side effect: its
    module graph drops from 6,756 to 1,318, and the new routes land at
    1,673-1,734, next to the shipped /v2/credentials/providers baseline of
    1,668.
    
    Supporting changes:
    
    - scripts/sync-tool-metadata.ts derives hostedApiKey ('always' |
      'conditional' | 'none') from each tool's `hosting`. The config itself
      stays excluded because it holds closures, but "does Sim host the key"
      is a first-order authoring question, so the answer is emitted.
    - getCopilotToolDescription takes hostedApiKey as an option instead of
      reading `hosting` off the tool, so both an executable ToolConfig and
      the generated metadata can answer it through one shared derivation.
    - principalUserId / allowedIntegrationTypes move out of
      lib/credentials/application/provider-catalog.ts into
      lib/integrations/principal-scope.server.ts. Two copies of the
      workspace integration gate would diverge first on the workspace-key
      path, which has no user for permission groups to key on.
    - scripts/check-tool-registry-boundary.ts walked page.tsx/layout.tsx
      under app/workspace only, so a route importing the executable registry
      passed green. It now walks a list of entry sources, seeded with the
      four catalog route subtrees and the shared projection barrel. Routes
      are covered per subtree rather than wholesale because 122 of ~1,130
      route files legitimately execute tools.
    
    Registry sweeps parse every block, tool, connector type, and enrichment
    through its published response schema and compare against the wire
    round-trip. They caught a real drift while being written: an operation's
    inputs were typed as a union of the tool-param and block-input shapes,
    and the union resolved to whichever member matched first, silently
    dropping a block input's `schema`.
    
    * docs(api): stop publishing a 413 GET /workflows/{id}/state cannot emit
    
    * feat(v2): read upload-session state
    
    Adds GET /api/v2/files/uploads/{uploadId}. Only DELETE was exported, so a
    caller that lost track of a transfer could abort it but could not ask
    whether the session was still alive, already finalized, or failed — the
    resume story was missing.
    
    Runs on a new files.upload.read operation at minimumRole 'read' rather than
    reusing uploadCancel, which is a 'write': asking about a session must not
    require permission to destroy it. The GET is a control leg like every other,
    so it carries the signed upload token and re-authorizes the caller's present
    workspace permission through reauthorizeWorkspaceUploadPurpose instead of
    resolving the session on its id alone.
    
    * fix(api): reconcile v2 catalog and workflow-authoring integration
    
    Merging the catalog and workflow-authoring branches surfaced four issues
    that neither produced in isolation.
    
    - Route and OpenAPI counters were bumped to the same value on both
      branches, so git merged them as one change while the merged tree holds
      the sum. Corrects the route ratchet to 1142 and the workflows document
      to 29 operations (152 total), then regenerates the OpenAPI documents
      and the CLI surface from the reconciled contracts.
    - The seven new workflow operations were published in the spec but absent
      from the workflow API reference groups, which `check:openapi` rejects.
    - `route-policies.ts` reached `WorkflowOperationsNotAppliedError` through
      `apply-workflow-operations`, dragging the edit engine — and its diff and
      comparison dependencies, which reach a client OAuth hook — into every
      route that uses the shared workflow error policies. The class moves to
      its own leaf module, mirroring `WorkflowImportError`, and each importer
      now takes it from there.
    - The operations route test shadowed that class inside its module mock, so
      `instanceof` matched a fake and the assertion pinned a message the
      production class never emits. It now uses the real class and asserts the
      real message.
    
    * feat(v2): extract ZIP archives over the public API
    
    Adds POST /api/v2/files/{fileId}/extract and widens files.extract_archive
    from principalKinds ['session'] / workspaceApiKey 'deny' to admit personal
    and workspace API keys at the unchanged 'write' role.
    
    The widening is an authorization change, so the justification lives in the
    operation's TSDoc: extraction grants no capability an API key lacks, since
    every file it writes could be created one at a time through files.create and
    files.upload.create, both already 'allow' at the same role. It only collapses
    many calls into one. The previous ['session'] restriction read as an artifact
    of the UI having been the only caller. Delegated services stay out — no
    copilot or executor caller exists and admitting one is a separate decision.
    
    The response is counts plus the destination folderPath, never the extracted
    files: a large archive would otherwise materialize thousands of objects into
    one body. Callers page GET /api/v2/files?folderPath=... instead. The use case
    returns the internal display path and the adapter projects it to a v2 path,
    keeping the use case surface-neutral.
    
    * feat(v2): extract file text over the public API
    
    Adds GET /api/v2/files/{fileId}/text. Text extraction previously sat behind
    checkInternalAuth on /api/files/parse, a route that also mixes in external-URL
    fetching, execution-file upload, and multi-file aggregation, so it could not be
    reused. The parse call is lifted into a thin application use case instead.
    
    Runs on the existing files.read_content operation unchanged — it is already
    workspaceApiKey 'allow' at the read role, and turning bytes it already
    authorizes into text grants no further reach.
    
    `degraded` is a required, non-optional boolean on the response. The legacy doc
    and ppt parsers deliberately return best-effort or placeholder content rather
    than throwing, so an omittable flag would let a client that never checks it
    treat guessed text as extracted text. It is reported honestly rather than
    converted into an error, because the parsers' behaviour is deliberate and
    characterization-tested.
    
    The read is bounded on its input at 25 MiB before extraction rather than on its
    output after, given the parsers' documented DoS history; a caller may lower the
    ceiling but never raise it.
    
    * feat(v2): restore archived folders and list the archived set
    
    DELETE /api/v2/files/folders archives recursively, so a recursive delete was
    unrecoverable over the API: the archived files stayed visible through
    GET /api/v2/files?scope=archived, but nothing could rebuild the folder
    structure.
    
    Adds POST /api/v2/files/folders/restore, path-addressed like the rest of the
    v2 folder family, and a `scope` selector on the folder list so a caller can
    find the archived path to hand it.
    
    `scope` extends the files folder-list query rather than the shared
    v2ListFoldersQuerySchema: only workspace files have an archived folder set, so
    adding it to the shared schema would give tables, workflows, and knowledge a
    parameter they ignore. GET /api/v2/files/folders is a FULL_SET_LIST, not paged,
    so no cursor binding changes — list-pagination.test.ts passes unchanged.
    
    Restore resolves the archived folder from its path by scanning the archived
    set rather than walking the live tree, which by definition does not contain
    the folder being restored. The folder-restored analytics hook now reports the
    folder actually restored rather than the requested selector, which carries no
    id on a path-addressed surface.
    
    * feat(v2): bulk-download a file selection as a zip
    
    Adds GET /api/v2/files/bulk-download, an adapter over the existing
    downloadWorkspaceFileItems use case and its internal binary route.
    
    Path collision: a static segment beside [fileId] permanently shadows a file
    whose id equals it, and workspaceFileIdSchema does accept [A-Za-z0-9_-]+.
    Rather than invent a new shape, this follows the existing bulk-delete sibling:
    the hyphenated form cannot be produced by either minted id shape (UUID v4 or
    wf_<shortId>), so the shadowed id is unreachable in practice. Documented on
    the contract so the reasoning is not lost.
    
    Folders are addressed by path, matching the rest of the v2 file surface. The
    paths resolve against the folder set the selection already loads, so it costs
    no extra query, and a path matching no folder is rejected rather than silently
    dropped — a misspelled folder must not yield a zip of whatever else was
    selected. The empty-selection and folder-count guards now account for
    folderPaths, which a path-only selection would otherwise have tripped.
    
    Selections are comma-separated only: v2 rejects a query parameter sent more
    than once, so a repeated-parameter form would never reach the schema. Pinned by
    a test so the contract cannot advertise a form the boundary rejects.
    
    * feat(v2): expose run output files and optional inline bytes on the runs read
    
    GET /api/v2/workflows/{id}/runs/{runId} now reports the files a run produced,
    each with the downloadPath that fetches its bytes, and can inline them as
    base64 on request.
    
    Gated by includeOutput, matching `output`'s nullability: a caller that did not
    ask for output does not receive a file list it did not request. The async
    execute request's rejection of includeFileBase64 is deliberately left alone —
    at submit time the run has not happened, so there is nothing to inline; reading
    a finished run is the first moment the question means anything.
    
    Inlining is capped per file at the executor's 16 MiB inline ceiling, which a
    caller may lower but never raise. A file above it answers 413 naming that
    file's downloadPath, so the caller is told exactly how to get the bytes rather
    than being left stuck.
    
    The descriptor deliberately omits the storage key — files are addressed by id
    and the key is re-derived from the run's recording — and omits an expiry, which
    the recording does not carry and which would be fabricated if published.
    
    The route becomes headSafe: false, since inlining reads object storage. The
    builder enforces that this requires the use case to expose authorize(), so HEAD
    still answers from a real authorization rather than from authentication alone.
    
    * feat(v2): permanently delete an archived file
    
    DELETE /api/v2/files/{fileId} only archives — the OpenAPI says its stored bytes
    are never removed — so there was no way to actually destroy a file over the API.
    Adds the repository primitive, application use case, operation, and
    DELETE /api/v2/files/{fileId}/permanent.
    
    A distinct path rather than a flag on the ordinary delete: a query parameter
    that turns a recoverable archive into an irreversible destruction is set by
    accident, and the two acts carry different minimum roles, which one route
    declaration cannot express. The file must already be archived; a live file
    answers 409 naming the archive step, so no single request can turn a live file
    into lost bytes.
    
    minimumRole 'admin', which forces workspaceApiKey 'deny' since the workspace-key
    ceiling is 'write' — the desired policy anyway: unattended credentials should
    not destroy bytes.
    
    Row first, then object. The two legs commit independently, so one can survive a
    crash between them: deleting the row first leaves at most an orphaned object for
    the storage sweep, while the reverse would leave a live row pointing at bytes
    that no longer exist — a file that lists and opens but can never be read. A
    failed object delete is therefore reported as objectDeleted: false rather than
    thrown, because the request has genuinely succeeded once the row is gone. Both
    directions are pinned by failure-injection tests, verified to fail when the
    order is reversed.
    
    Audited as a distinct FILE_PERMANENTLY_DELETED action, not a reuse of
    FILE_DELETED, which records the recoverable archive step.
    
    * feat(api): v2 log analytics, itemized cost, filters, and sortable query
    
    Adds the aggregate and rich-read halves of the public logs surface, and
    fixes three defects the existing reads carry.
    
    Aggregate analytics. `GET /api/v2/logs/stats` returns time-bucketed run
    counts, success rate, error count, mean latency, and the window bounds,
    per workflow and for the workspace. The first-party route was a raw
    handler with inline SQL and inline aggregation, so it is split into a
    repository (`lib/logs/stats-queries.ts`), a pure aggregator
    (`lib/logs/stats.ts`), and an application use case. That route keeps its
    legacy authorization — it answers a caller without workspace access with
    a zeroed 200, where v2 conceals the workspace as a 404 — and consumes
    only the two surface-neutral halves.
    
    `segmentCount` had no `.int()`, `.min()`, or `.max()`, so `0` divided by
    zero and `1e9` allocated two billion-element arrays: both caller-reachable
    500s. Bounded on both contracts. `workflows` is capped, with the workspace
    totals still computed from every workflow and the cut reported as
    `workflowsTruncated`.
    
    Detail reads gain the itemized `cost.items` ledger (`null` and `[]` are
    distinct answers and both reachable) and `workflowInput`, restoring a
    v1→v2 regression.
    
    The list gains `workflowName` and `status` filters, and `includeJobRuns`,
    which unions Chat and Sim-agent job runs into the sequence behind a new
    `kind` discriminator — without it a job run is indistinguishable from a
    run whose workflow was deleted. A filter no job row can answer drops the
    branch outright rather than meaning two things across the union.
    
    `POST /api/v2/logs/query` carries the additional sort columns. `GET /logs`
    is untouched: its single `order` param rests on there being exactly one
    sortable column, and both escapes from that are ruled out, so the rich
    read gets its own endpoint — the split the table surface already ships.
    It uses the shared keyset scheme with the two nullable sort columns read
    through a sentinel, since a keyset cannot compare against null.
    
    `folderPaths` now covers a folder's whole subtree on the public path, as
    it already did everywhere else; it previously omitted every nested run
    with no error. The path strings did not change, so a folder-scope version
    is stamped into the cursor and in-flight tokens restart rather than
    silently skipping rows.
    
    Also fixes `folderName`, which ILIKEd `workflow.name` — a copy of the
    clause above it — and so searched workflow names instead of folders.
    
    `buildLogSortCursorCondition`'s `IS NULL` disjunct is documented and
    pinned: under `NULLS LAST` the null block is only reachable through it,
    so removing it as a duplicate-row fix makes those runs unpageable.
    
    Ratchets: route count 1142 -> 1144; logs OpenAPI operations 2 -> 4; total
    operations 152 -> 154.
    
    * feat(api): v2 tables run state, dispatch polling, batch update, bulk, archive
    
    Closes the headless gaps on the v2 tables surface.
    
    - Per-cell run state is now readable through an opt-in `includeRunState` on
      `GET /rows`, `POST /query`, and `GET /rows/{rowId}`. The default projection
      is byte-identical; a page whose sidecar outgrows its byte budget is a 413
      rather than a silent truncation.
    - Run dispatches are addressable: `GET /tables/dispatches/{dispatchId}`
      publishes the column's full four-state domain so polling a finished run is
      not a 500, and `GET /tables/{tableId}/dispatches` lists what is in flight.
    - `POST /rows/batch-update` takes one distinct patch per row. Its transaction
      moved out of the Copilot-only module into a surface-neutral use case both
      surfaces now call.
    - `GET .../enrichment/{groupId}` publishes the provider cascade, cost, and
      timing behind one enrichment cell.
    - `POST /tables/bulk-move` and `/bulk-delete` reach the existing bulk use
      cases, which now accept folders by canonical path and resolve them inside
      the application layer.
    - `DELETE` is recoverable: `scope=archived` on the table list plus
      `POST /tables/{tableId}/restore`.
    
    * feat(api): expose knowledge chunks, tag writes, archive/restore on v2
    
    Closes the knowledge cluster's remaining public-surface gaps.
    
    Chunks: list/read/create/update/delete/bulk under
    `/api/v2/knowledge/{id}/documents/{documentId}/chunks`. `queryChunks` gains
    an `id` tiebreaker on every sort so the list pages on a keyset rather than an
    offset — `tokenCount` and `enabled` are both non-unique, so a page boundary
    inside a run of equal values used to repeat or drop the tied rows. The
    internal offset caller is unchanged; the two positioning schemes share one
    read.
    
    Tag definitions: create, update, delete, next-slot, usage, and the
    document-scoped save and cleanup. Without them a caller could write a tag
    value into a slot with no definition and then had no way to name it, so
    tag-filtered retrieval was unbuildable end-to-end. `v2KnowledgeTagSchema`
    gains `id`, without which PATCH and DELETE are unaddressable. The
    document-scoped DELETE is pinned to `action: 'cleanup'`: the domain's `'all'`
    deletes the whole knowledge base's tag vocabulary from a document path.
    
    Archive/restore: `GET /api/v2/knowledge/archived` as a sibling route rather
    than a `scope` param — the two reads bind different operations and a v2 route
    declares one — plus `POST /api/v2/knowledge/{id}/restore`. `knowledge.restore`
    is a new workspace operation carrying `delete`'s policy, since an operation's
    inverse must not be harder to reach; the internal session route now delegates
    its workspace branch to the shared use case and keeps only the legacy personal
    one.
    
    Also: `POST .../documents/from-workspace-files` surfaces `addWorkspaceFiles`,
    so a file already in workspace storage no longer has to be re-uploaded
    byte-for-byte to be indexed; the `chunkingConfig` write widens to the
    first-party five-key schema with its refines and separator bounds, while the
    response stays `.catchall` so a legacy JSONB row cannot 500; and
    `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` joins `FORBIDDEN_DETAIL_CODES` now that
    the bare 403 on connector-managed chunk writes is wire-reachable.
    
    Document upsert is deliberately not included.
    
    * feat(api): add v2 credential rotation and a gate-exempt capabilities endpoint
    
    PATCH /api/v2/credentials/{credentialId} rotates service-account secret
    material or renames a credential in place, preserving the credential id so
    existing workflow, deployment, paused-run, connector, and webhook references
    keep working. Re-posting to POST /api/v2/credentials answers 409, and
    delete-and-recreate mints a new id, so rotation previously had no door.
    
    The route is adapter-only: updateWorkspaceCredentialUseCase already owned the
    rotation, its audit projection, and credentials.update. It gains one additive
    assertedWorkspaceId field for the v2 workspace assertion, and the per-principal
    credential-type table that deleteCredentialUseCase already applied is lifted
    into requireManageableCredentialType so both operations share it. Without it a
    personal API key could rename an env_workspace row and toV2Credential's throw
    would surface as a caller-reachable 500.
    
    CredentialProviderOperationError now maps to 503 with Retry-After when the
    provider is unreachable, instead of the 400 its OrchestrationError('validation')
    base projected. A transient outage rendered as a permanent input error invites a
    caller to revoke a working credential.
    
    GET /api/v2/meta reports the calling key's rollout cohort, type, and expiry.
    It is the one route declaring the new typed gate: 'exempt' option, because the
    rollout gate and the unknown-path catch-all answer byte-identical 404s and a
    gated /api/v2/meta could never resolve that ambiguity. Authentication still runs
    first, so the only fact disclosed is one about the caller's own credential.
    
    * feat(api): publish deployment lifecycle and workflow-MCP v2 surfaces
    
    Adds the four deployment-lifecycle operations v2 was missing, and the
    workflow-as-MCP publishing surface, both as adapters over application use
    cases that already existed.
    
    Deployment lifecycle:
    - PATCH /api/v2/workflows/{id}/versions/{version} relabels a version.
      Deliberately not the internal route's body-shape dispatch between
      "rename" and "promote to live".
    - POST .../versions/{version}/activate promotes a version. Same use case
      as rollback under a different transition, on its own path because the
      two mean opposite things to a caller.
    - POST .../versions/{version}/revert overwrites the draft. Accepts the
      literal `active` alongside a version number.
    - PATCH /api/v2/workflows/{id}/deployment toggles unauthenticated public
      execution.
    
    `workflows.public_api.update` widens from session-only to session plus
    personal API key: it is an admin-role change the same accountable human
    may make from a script. Workspace keys stay denied. Its EE refusal now
    carries PUBLIC_SHARING_NOT_ALLOWED instead of a bare forbidden.
    
    Workflow MCP servers:
    - /api/v2/workflow-mcp-servers list, create, update, delete, plus
      publish and unpublish of a workflow as a tool. Named apart from
      /api/v2/mcp-servers, which registers the external servers Sim calls.
    - The six mcp_servers.workflow_deployments operations widen from
      ['delegated'] to admit sessions and personal API keys; roles and the
      workspace-key denial are unchanged.
    - The server list gains keyset pagination, matching its external
      sibling, since nothing caps how many a workspace publishes.
    - Server, tool, and workflow reads move out of the use case into
      lib/mcp/queries.
    
    Route ratchet 1150 -> 1160; OpenAPI operations 161 -> 171.
    
    * feat(api): extract chat deployments and publish the v2 surface
    
    Chat deployment was a shipped module with no public API and two
    authorization systems: `lib/workflows/application/chat-deployments.ts`
    had deploy/undeploy extracted, but only Copilot used them — the REST
    routes reimplemented workflow authorization inline, and `PATCH
    /api/chat/manage/[id]` additionally owned password encryption, the
    auth-type field-clearing matrix, identifier uniqueness, the
    redeploy-gating protocol with two 409s, a raw db.update, and a manual
    recordAudit.
    
    New `lib/chat-deployments` domain:
    - `chat_deployments.list/read/update/delete`, keyed on the deployment
      whose workspace is derived by joining its workflow. Creation stays
      `workflows.chat.deploy`, which is keyed on the workflow.
    - The PATCH extraction, including the field-clearing matrix and the
      asynchronous-cutover invariant the route had hand-mirrored from
      `performChatDeploy`.
    - One `buildChatDeploymentUrl`, replacing three constructions that had
      already drifted onto two different host helpers. There is no chat
      subdomain, so nothing publishes a host.
    - Repository reads moved out of the use cases into
      `lib/chat-deployments/queries`.
    
    Internal routes are now adapters over those use cases. `GET /api/chat`
    is deliberately not migrated: it scopes by `chat.userId` while every
    other chat operation authorizes by workspace admin, and reconciling the
    two is a product decision. `PATCH` keeps its 400 for an identifier
    collision through a typed `ChatIdentifierInUseError`; v2 reports the
    409 the condition actually is.
    
    v2 surface at `/api/v2/chat-deployments`: list, create, read, update,
    delete. Workspace-scoped, keyset-paged, and a stored password is never
    readable — reads carry `hasPassword` only, and the session-only reveal
    endpoint deliberately has no v2 counterpart.
    
    Also: an email- or SSO-gated chat with an empty allow-list is now
    refused in the use case rather than only at the internal boundary, since
    it is unenterable; and the doc comment on `processHostedKeyCost`
    claiming a `usageLog` write is corrected — no such write exists.
    
    Route ratchet 1160 -> 1165; OpenAPI operations 171 -> 176.
    
    * fix(api): close three review findings, two of them caller-reachable
    
    - Run output files are filtered to keys under the run's own execution
      prefix. The recording they came from is not a trustworthy key source:
      the start block copies every caller-supplied input field verbatim into
      its output and `collectUserFilesById` accepts anything carrying the
      `UserFile` shape, so a caller could name any storage key and have the
      download and base64 paths — neither of which authorizes per file — serve
      it back.
    - `getBlock` reads own keys only. `BLOCK_REGISTRY` is an object literal,
      so `constructor`, `toString` and friends returned inherited functions
      that every consumer then treated as a block, turning a path segment into
      a 500. `getToolMetadata` already guarded this way.
    - A folder-scoped log page no longer unions in every job run in the
      workspace. The guard read `filters.folderIds`, which the public surface
      never sets — it carries the folder filter in `folderScope` — so the page
      contradicted the contract's promise that job runs are dropped whenever a
      filter they cannot answer is set.
    
    Also: the log cursor stamps `includeJobRuns` only when it is on, so its
    `.default(false)` no longer puts a constant in every fingerprint and
    rejects cursors minted before it existed; and the `folderName` subquery is
    scoped to the workspace and to workflow folders instead of scanning the
    whole `folder` table.
    
    * fix(api): close two more review findings, one an authorization bypass
    
    - `workflows.operations.apply` no longer admits a workspace API key. The
      use case authorizes against three per-user policies — the EE permission
      config, block visibility, and credential reachability — and all three
      take a human subject. An actorless key has none, and both substitutes
      fail open: attributing to the workspace billing owner evaluates the
      batch as the least-restricted account in the workspace, and passing no
      user makes `getUserPermissionConfig` return `null`, which every caller
      reads as unrestricted. Either way a workspace constrained by an
      allowlist was edited as though it were not. Personal keys keep the
      capability, so headless editing is unaffected for a credential that
      names a human.
    - `GET /workflows/{id}/state` reads its variables through
      `parseWorkflowVariables`, and the stored variable response schema drops
      the two assertions the column cannot honour. The column has carried a
      JSON string and a legacy array as well as the current record, the
      realtime `variable.add` op types `type` as `z.any()`, and the parser
      writes `name` through verbatim — so the input bounds on the read turned
      a stored workflow into a 500 on the endpoint that opens it. The write
      schema keeps them, which is where they can still be honoured.
    - `GET /workflows?scope=archived` projects folder paths tolerantly.
      Archiving a folder cascades onto the workflows inside it but leaves
      their `folderId` dangling — which is why restore has to null it — so the
      strict projector threw a bare `Error` and took the whole page down with
      no cursor able to step past the row.
    
    * fix(files): bind Start-block file keys to the executing workspace
    
    The Start block derived a file's storage key by parsing the caller's own
    `url`, which `isInternalFileUrl` matches on any host and
    `extractStorageKey` returns verbatim — so a request body could name any
    tenant's bytes. The key is now accepted only when its own layout names
    the workspace the execution runs in, and every file is dropped when the
    execution carries no workspace.
    
    Also bounds `includeFileBase64` with an aggregate response ceiling and a
    worker pool instead of an unbounded `Promise.all`, scopes the bulk
    download's authorization resource to the workspace when folder paths are
    requested, makes the folder-restore selector mutually exclusive at the
    type level, and names the bound in the `maxBytes` validation message.
    
    * fix(api): close v2 log review findings
    
    Cursor scope: `scope` on the workflow and table lists carries
    `.default('active')`, so it entered every fingerprint as a constant and
    refused every cursor minted before the param existed — with the
    "cursor does not match the requested filters" 400, which is actively
    misleading for a caller that changed nothing. Both now stamp the
    default as absent, so only a caller who asked for `archived` gets a new
    sequence.
    
    Dashboard stats: `maxWorkflows` capped the response, not the
    allocation. Segment series are now densified after the cut instead of
    before, so returning 200 series no longer materializes one
    `segmentCount`-length array per workflow in the window. The aggregate
    still sums every workflow, now from the sparse per-workflow maps.
    
    Cost keyset: `cost_total` is an unconstrained `numeric`, so its anchor
    travelled through `Number()` and was compared back at full precision —
    rows differing beyond float64 collapsed onto one anchor. Adds
    `decimalKey`, which carries the digit string and binds it `::numeric`.
    
    Run detail: `cost_total` is a backfilled projection, so a run predating
    the backfill reported `cost: null` even with a real ledger, making
    `items` unreachable for exactly the runs the ledger explains. Falls
    back to the ledger total.
    
    Also caps the log folder-path index reads at MAX_FOLDERS_PER_WORKSPACE
    like every other reader, publishing the folder-tree 413 on the four log
    operations; reverts a dead `status` widening in `v2CommaListSchema`;
    drops an unread `executionData` select; corrects the segment-count and
    searchLogs prose; and replaces the sort-cursor SQL-text assertions with
    a two-page walk over a fixture with a null block.
    
    * fix(api): close knowledge v2 review findings
    
    - widen knowledge.list_archived to the delete/restore policy so a workspace
      API key can discover what it may restore
    - escape LIKE wildcards on the now-public chunk search
    - derive tag slot capacity from TAG_SLOT_CONFIG per field type
    - type updateKnowledgeBase's chunkingConfig as ChunkingConfig and project
      every declared field explicitly
    - attribute a restore to the calling surface instead of a literal 'api'
    - gate 'knowledge chunks batch-update' behind --yes, since it can delete
    - present the tag-cleanup action from the parsed request rather than
      faulting on the domain result after the delete committed
    - unbind asserted-scope workspaceId from the nested knowledge cursors,
      matching the table-row lists
    - add executed-SQL coverage for the chunk keyset
    
    * fix(catalog): close the catalog and registry-boundary review findings
    
    The module-graph ratchet treated an entry with no baseline row as
    informational, so the six catalog routes and the projection barrel were
    unratcheted while the summary still read "within their module-count
    baseline". An unbaselined entry now fails --check, the summary counts only
    what was actually compared, and the baseline is re-recorded.
    
    The Copilot block-metadata tool — the reason the shared projection exists,
    6,756 modules down to 1,321 — was in no guarded subtree. It is now an entry
    source and a catalog boundary root.
    
    Catalog behaviour:
    - hostedApiKey is gated on the deployment, so a self-hosted install reports
      none instead of promising 127 tools' keys it will never supply
    - block detail resolves an unversioned base type to its newest version and
      projects through the viewer's visibility, so it can no longer 404 a block
      the list contains or name it differently
    - offset-cursor ordering compares code units rather than the process locale
    - projections copy every array they publish instead of handing out the
      registries' own
    - an options function returning a thenable throws rather than silently
      widening the providers-store substitution across the event loop
    - a throwing block projection costs the Copilot tool one block, not all of them
    - the trigger-kind log returns to debug: chat/manual/api are entry-point
      kinds, not authoring defects
    
    Also sweeps the custom-block detail branch against its response schema,
    guards each projection module rather than the dead barrel over them, and
    drops a provably dead branch in processHostedKeyCost.
    
    * fix(workflows): close v2 workflow-authoring review findings
    
    - Read a blockless draft back as an empty graph. `PUT /state` of
      `{ blocks: {}, edges: [] }` — the contract's own published example —
      deletes every block row, and the loader answers `null` for a blockless
      workflow, so the following `GET /state` answered 404 while the list
      endpoint still showed the workflow. Existence is the workflow row's to
      decide; the null is now projected as an empty graph.
    - Rewrite the `readWorkflowGraph` authorization test so it can fail. It
      called `authorize?.()` and asserted only a negative, so deleting
      `authorize` or replacing it with a no-op both passed — the invariant the
      head-safe `HEAD` path depends on.
    - Route `setWorkflowBlockEnabled` through `replaceWorkflowNormalizedState`,
      the same door the other two graph writes use, instead of writing the
      normalized tables itself without state preparation or custom-tool
      extraction.
    - Count applied operations directly. Enablement refusals landed in the same
      skipped-item array and were subtracted from the operation count, which
      `Math.max(applied, 0)` then masked when it went negative.
    - Give a `disabled_ancestor` refusal its own member of the published skip
      enum instead of reporting it as `block_locked`.
    - Refuse an `atomic` batch whose credential or hosted API key would be
      stripped, and carry the dropped inputs in the 409 details.
    - Publish the whole lint report — `sources`, `sinks`, `orphanBlocks`,
      `emptyOutgoingPorts`, `invalidBranchPorts`, `invalidConnectionTargets`,
      `fieldIssues`, and the `kind` discriminator on unresolved references —
      rather than only free-text reference prose.
    - Stop reporting unresolved lint references as `inputValidationErrors`.
      `collectUnresolvedReferences` is read-only, so those values stay
      persisted; they were double-reported, and falsely as dropped inputs.
    - Replace two unfalsifiable negative-principal tests, which used a
      principal kind `Exclude`d from `PrincipalKind`, with a reachable one.
    - Nits: drop a stranded TSDoc block; assert the membership predicate in the
      selector-validator admin test; make the HEAD test assert a representation;
      assert the sanitized graph is what `replaceWorkflowState` writes; add a
      route test rejecting `baseGraph` in a v2 body; carry
      `principalAuditSource` on restore/duplicate/moveBulk audit; unify the two
      `base64MaxBytes` ceilings on `MAX_INLINE_MATERIALIZATION_BYTES`.
    
    * fix(api): close seven v2 review findings, one an authorization bypass
    
    Raise mcp_servers.workflow_deployments.update_server to admin: its body
    carries isPublic, and a public server executes with no Sim credential, so a
    write member could remove authentication from every workflow it publishes.
    create_server already grants the same visibility at admin.
    
    Pin the widened operations in registry tests — the six workflow-MCP ones,
    the four workflow widenings, and files.extract_archive.
    
    Reject secret fields on a credential that has no rotatable secret instead of
    dropping them behind a 200, classify a non-transient provider 4xx as caller
    error rather than a retryable outage, and reconcile a provider outage to 503
    with Retry-After on all three surfaces.
    
    Give /v2/meta a declarative principal policy through a new defineOperation
    factory, carry the key expiry on the auth context instead of reading the
    api_key table from the application layer, and make the impossible principal
    branch an invariant error rather than a codeless 403.
    
    Enforce the rollout-gate exemption at definition time, against the one
    contract path it is reserved for, and remove the gate parameter from the
    exported admission helper so the builder is its only door.
    
    * fix(tables): bound the run-state sidecar, and close six v2 review findings
    
    Enforces the 2 MiB run-state ceiling INSIDE the sidecar drain rather than
    over its materialized result, refuses the unbounded query form paired with
    it, and normalizes the two stored blobs the v2 surface publishes from bare
    `as` casts.
    
    - The run-state budget now travels into `loadExecutionsByRow`, which drains
      row ids in bounded chunks and refuses before fetching the next one. The
      post-hoc `requireBoundedRunState` walk is gone: it measured a spike that
      had already happened, and re-serialized every entry to do it.
    - Both row reads that accept `includeRunState` cap the page at
      `V2_MAX_RUN_STATE_ROW_LIMIT`, and `POST /tables/{id}/query` additionally
      refuses the flag paired with `limit: 0`.
    - `runState.status` and the enrichment cascade blob are projected onto the
      published shape before presentation; both were caller-reachable 500s on a
      well-formed read.
    - `sim tables bulk-delete` now gates behind `--yes`, and the CLI sweep that
      should have caught it covers destructive non-DELETE forms.
    - `POST /tables/{id}/restore` is idempotent (200, no audit) like its
      knowledge sibling, and bulk folder selection deduplicates after resolution.
    - The batch-update backstop keeps the looser Copilot ceiling and says so in
      TSDoc: it is a backstop no surface reaches, because the contracts stop a
      v2 caller at 1000 and the Copilot tool stops itself at 5000. Each caller
      sees the bound that actually applies to it; neither surface's cap moved.
    
    * fix(chat-deployments): close v2 chat review findings
    
    Fixes the chat-deployments slice of the v2 review, several of which are
    regressions the application-operation extraction introduced.
    
    - Stop a `500` on schemaless JSONB: the response now declares a stored
      shape without bounds and `toV2ChatDeployment` projects
      `customizations`, `outputConfigs`, and `allowedEmails` onto it. The
      request schemas keep `.strict()` and their bounds.
    - Restore the specific validation message on `POST /api/chat` and
      `PATCH /api/chat/manage/[id]`, and the deleted test that pinned it.
    - Restore `chat_deployments.read` to workspace `admin`; the detail read
      serves the visitor gate.
    - Narrow the list projection so `chat_deployments.list` can stay a
      `read` operation reachable by a workspace API key: `allowedEmails`,
      `hasPassword`, and `customizations` are gone from the list entry and
      available only from the admin-gated detail read. Serialized field by
      field so a field added to the detail shape cannot reach the list by
      default.
    - Classify create-path failures: `performChatDeploy` carries an
      `errorCode`, so an in-flight deployment is a `409` and an invariant
      failure a `500` instead of every refusal being a `400`.
    - Delete the callerless `GET /api/chat`, which served the encrypted
      password column with no response contract.
    - Propagate undeploy infrastructure failures instead of concealing them
      as `404`, and return `ChatDeploymentView` from both delete paths.
    - Name `CHAT_AUTH_MODE_NOT_PERMITTED` on the create path.
    - Guard `getBaseUrl` inside `buildChatDeploymentUrl`, which otherwise
      throws on a self-host with no `NEXT_PUBLIC_APP_URL`.
    - Correct the published allow-list claim: a replacement `allowedEmails`
      is applied after the auth-type clear, so it does survive.
    - Assert `workspaceId` on the v2 detail routes and reconcile the
      concealment TSDoc with what the error policy actually renders.
    - Move `resolveActiveWorkspaceApplicationContext` to the workspaces
      domain so chat-deployments no longer imports workflow application code.
    
    * test(credentials): pin the reconciled provider-outage status
    
    The internal route alone answered 502 where the v2 surface, the shared
    status helper and `PROVIDER_OUTAGE_CODES`' own TSDoc all say 503. The
    test pinned the divergence; it now pins the reconciliation, including the
    `Retry-After` a 503 carries. Corrects a stale comment that still named
    502 as the value callers see.
    
    * fix(executor): restore cloud-storage Start files, dropped by the key rule
    
    The ownership check accepted a key only when it could be parsed out of an
    internal `/api/files/serve/...` URL. But the server-side uploader for run
    inputs returns a *presigned cloud* URL whenever object storage is
    configured, whose path is the bucket key — so every chat-deployment
    attachment, API `files[]` payload and generic-webhook file field resolved
    no key, and because normalization is all-or-nothing the entire `files`
    input was dropped with no error. It passed locally and under vitest only
    because the uploader falls back to an internal URL when no object storage
    is configured, which is exactly why no test caught it.
    
    The test is ownership, not provenance: a key is accepted when its own
    layout names the executing workspace, whether it arrives directly or is
    parsed out of the URL. Neither field has to be trusted, since both are
    caller-authored and both are held to the same check. A payload whose key
    and URL disagree is refused rather than resolved in the caller's favour —
    a genuine uploader writes the two consistently, so only a forged pairing
    is turned away.
    
    `context` is now derived from the accepted key rather than read from the
    payload or the URL's `?context=`, so an owned key can no longer be
    labelled with a bucket its bytes do not live in — the hardening the
    previous comment claimed but did not perform.
    
    * fix(api): close four defects the fix pass introduced
    
    - `resolveLatest` built a `RegExp` from the caller's block id and read the
      registry with a bare lookup, so the catalog detail route — moved onto it
      by the version-alias fix — routed around the `ownBlock` guard added for
      exactly this. `GET /api/v2/blocks/%5B` was a `SyntaxError` 500 and
      `.../constructor` an inherited function. Matched by string comparison
      now, the way `tools/tool-ids.ts` resolves the same convention, and read
      through `ownBlock`.
    - The run-state byte budget was applied inside `queryRows` rather than at
      the callers that publish it, so the first-party table grid — which reads
      run state at five times the row limit and publishes no ceiling — turned
      a large page into a hard failure, with an error naming a parameter it
      does not expose. The budget is now an explicit option the public reads
      pass and internal callers omit.
    - Three graph-write CLI commands shipped ungated because the sweep meant
      to catch them matched only the names already enumerated, so it could
      never fail. It now forces every non-`GET` operation into a destructive
      or non-destructive list, and the three carry confirmations.
    - Unbinding `workspaceId` from the knowledge-documents cursor was right on
      the merits and wrong in effect: the value is constant per sequence, so
      removing it changed the fingerprint and refused every cursor already in
      flight. Restored there; the chunks list is new in the same change and
      keeps the cleaner reading.
    
    * fix(api): resolve a detail read to a version the viewer can see
    
    `getLatestBlockForViewer` took the newest version and then hid it, which
    inverted the contradiction it was written to close: `slack_v2` and
    `table_v2` are preview-gated while their v1 deliberately stays in the
    toolbar, so an unrevealed viewer got a `404` on a detail read for a type
    `GET /api/v2/blocks` was listing in the same breath. It now walks versions
    newest-first and answers with the first one visible to that viewer.
    
    Also:
    - The chat password guard ran after `performFullDeploy`, so a request that
      could never succeed burned a real workflow deployment version and then
      answered 400. Its two sibling gate guards already refuse ahead of the
      deploy; this one now does too.
    - The Copilot sub-block serializer published the registry's own `options`
      and `dependsOn` arrays by reference. Pre-existing, but the catalog
      projection this parallels copies every array it publishes precisely
      because they are process-global and shared by every request.
    
    * fix(api): restore the locked read-modify-write and the password validator
    
    Two findings verified as real regressions against staging, out of ten
    checked — the rest were pre-existing, latent, or false.
    
    `setWorkflowBlockEnabled` read the graph outside the row lock and wrote it
    back inside a later transaction. The editor's own save takes that same
    lock, so an autosave committing in the window was silently discarded: this
    operation writes a whole graph, not a delta. The persistence primitive now
    accepts a reader that runs after the lock is taken, and the toggle
    re-reads and re-decides there. Its lock predicate is also scoped to the
    workspace and to a live row again, so a workflow archived mid-flight is
    refused rather than written.
    
    The v2 chat-deployment contracts inlined their own password rule twice
    instead of using `chatDeploymentPasswordSchema`, losing the refusal of a
    whitespace-only password — which the internal contract rejects precisely
    because it strands the deployment behind a password the visitor form will
    not submit. Both sites use the canonical validator now.
    
    * fix(api): one folder projection, one dynamic-provider list, honest 413s
    
    - `toV2Folder` existed twice, and the second copy had been written without
      the name/path invariant — so a row the list read refuses loudly would
      have been served with a mismatched pair by the restore read. One
      definition, guard included.
    - The catalog projection restated `DYNAMIC_MODEL_PROVIDERS` and had
      drifted by one member. Derived from the canonical list instead.
    - The tables reads documented a `413` for run state that they cannot emit
      — the budget became opt-in, and the row limit is the bound now — so the
      claim is removed rather than declared. The workflow run read has the
      opposite problem: it genuinely emits one, on a single file *or* the
      run's inlined total, and declared neither. Now declared, and the
      sentence covers both.
    - Reclassifies the operations staging added into the destructive sweep, so
      the triage stays exhaustive.
    
    * fix(v2): classify storage and uniqueness failures, drop permanent file delete
    
    - remove the permanent file-delete endpoint; the platform offers no such
      action in the UI, and its manager wrote outside a transaction with no
      storage accounting
    - extract a generated document's text from its compiled artifact rather than
      its generation source, matching the download path; a `.pdf` source was a
      500 and a `.docx` source returned generator JavaScript as clean content
    - report a run file whose object retention has already swept as 404 rather
      than 500, on both the inline base64 read and the download stream
    - report a knowledge tag that loses at a unique index as 409, naming whether
      the slot or the display name is taken
    - gate `workflows versions revert` behind a CLI confirm; it overwrites the
      draft graph and was classified non-destructive
    
    * feat(v2): report lint from both graph writes and add dry-run previews
    
    - `PUT /workflows/{id}/state` now returns the same `lint` report as
      `POST /operations`; an agent authoring a graph from scratch needs the
      findings at least as much as one editing incrementally
    - extract the report into one shared builder so the two writes cannot drift,
      and one shared presenter so the wire shape is identical
    - skip the credential/tool reference pass when the caller has no human
      subject, rather than resolving it against the workspace billing owner:
      that would misreport what the workflow can reach and disclose another
      person's grants. `lint.notes` says when it was skipped
    - add `?dryRun=true` to both graph writes: validates and lints, persists
      nothing, records no audit, notifies nobody. A query param, not a body
      field, since the body of a PUT is the resource itself
    - CLI: a dry run no longer demands `--yes`; requiring confirmation to preview
      a change teaches callers to pass `--yes` reflexively
    - CLI: name the graph commands for their verbs — `workflows state get`,
      `workflows state replace`, `workflows operations apply` — instead of the
      derived `state list` / `state update` / `operations create`
    - document when to use `rollback` vs `versions/{version}/activate` on both
    
    * chore(docs): sync generated docs manifest for the new CLI pages
    
    * feat(v2): add the missing workflow-MCP reads and align bulk naming
    
    - add `GET /workflow-mcp-servers/{serverId}` and
      `GET /workflow-mcp-servers/{serverId}/tools`. The resource could be
      PATCHed and DELETEd but never read, and its tools could be published and
      unpublished but never listed — the server list reports tool names only, so
      nothing published the `workflowId` that addresses a tool for deletion.
      Both mirror `mcp-servers` beside them, and carry that family's
      workspace-API-key denial rather than the wider `mcp_servers.read` policy
    - rename `POST /tables/bulk-move` to `POST /tables/move`, so tables matches
      the shipped `files` resource exactly (`move` + `bulk-delete`)
    - name the CLI commands for their operations instead of the derived
      `... create`: `tables move`, `workflows move`, `tables bulk-delete`, and
      `tables rows update-each` for the per-row batch, which sits beside the
      existing filter-based `tables rows batch-update`
    
    `POST /tables/{id}/rows/batch-update` keeps its name: a distinct payload per
    resource is precisely AIP-234 BatchUpdate, and `bulk-` would have collided
    one word away from the filter form.
    
    * fix(v2): correct documented statuses and a caller-reachable 500
    
    - duplicating a workflow into a locked destination folder answered 500:
      `FolderLockedError` is a plain Error carrying `status = 423`, which the v2
      error policy does not classify. Converted to OrchestrationError('locked')
      at the application boundary, matching the bulk-move path
    - restore workflow promised a 413 for an oversized folder tree that its
      response list never published; the cap is real, so the status now is too
    - move workflows and apply variables documented 409/423 they cannot emit:
      every per-item lock and conflict is reported in `failed`, not thrown
    - bulk download and delete knowledge tag can both 409 and did not say so;
      cleanup tag definitions cannot and did say so
    - apply workflow operations denies workspace API keys but never documented it
    - the dry-run responses are not byte-identical to a committed write:
      `needsRedeployment` describes the pre-write state and persistence warnings
      cannot appear. Reworded rather than overclaimed
    - read file text and get file upload are head-safe, so the "HEAD skips the
      effect" sentence did not apply to them
    
    * fix(v2): guard tag field-type changes and publish the 415 every body route can return
    
    - `PATCH /knowledge/{id}/tags/{tagId}` accepted a `fieldType` incompatible
      with the slot the tag already occupies. Slots are enumerated per field
      type, so a text tag could be relabelled `number` and every later read
      would interpret its values as the wrong type. Create checked this; update
      now runs the same two checks
    - derive `415` from the contract the way `413` already is: the JSON builder
      answers UNSUPPORTED_MEDIA_TYPE for any body under a content type it cannot
      read, so all 100 body routes could return a status none of them published
    - give version activation its own result component instead of publishing it
      as `RollbackResult`; the shipped rollback keeps that name
    - correct descriptions that promised behaviour the code does not have: a
      `processingStatus` field never returned, a `gmail_send` resolution example
      that short-circuits, bucket widths that overflow the window, a bulk tag
      save that relocates rather than overwrites, and per-server tool names
      actually gathered under a page-wide budget
    - drop 409 from three knowledge and upload reads that cannot emit it
    
    * docs(v2): correct the upload transfer contract and 16 other published claims
    
    The upload transfer step was documented as Sim's own data plane on every
    deployment: "success is 204" and "a failure is the v2 error envelope". That
    holds only when Sim stores objects itself. With object storage configured the
    URL is the provider's presigned URL, so S3 and GCS answer 200 and Azure 201,
    and a failure is the provider's XML — a client written to the old text reads a
    successful cloud upload as a failure. Also states that part ETags do not need
    retaining: completion takes no body because Sim lists the parts from the
    provider itself.
    
    Other corrections, all to shipped descriptions rather than behaviour:
    - DELETE table and bulk-delete files archive rather than erase, and neither
      said so; bulk delete also cannot emit the 409 it declared
    - complete knowledge upload published a 402 only the create leg can raise
    - billing status conceals a foreign workspace id as 404, not the 403 its
      TSDoc and description both claimed
    - audit entries null a folder's resourceId and strip folder ids from metadata
      at every level; neither redaction was documented
    - details=full adds the workflow summary to workflow runs only, never to job
      runs; GET /logs folderPaths covers a subtree like its two siblings; getLog
      now carries the retention sentence
    - list secrets returns description too, and the logs and resources documents
      described only part of what they serve
    
    * improvement(api): consolidate the v2 surface and close seven defects
    
    Endpoint consolidation:
    - Fold POST /logs/query into GET /logs; add sortBy/sortOrder, cap the
      comma lists, and move the list onto the shared keyset codec
    - Fold GET /knowledge/archived into GET /knowledge?scope=archived,
      matching files, tables, and workflows
    - Re-home chat deployments as a singleton under the workflow they
      belong to; keep the workspace-scoped discovery list
    - Move the tag-definition writes off the document path onto
      /knowledge/{id}/tags, where they already acted
    - Nest the table export and dispatch reads under their parent table
    
    Defects:
    - Publish isPublicApi on the deployment read; it was write-only, so a
      workflow could be opened to unauthenticated execution unauditably
    - Stop publishing raw storage keys and an unusable URL in log files
    - Classify a chat-identifier unique violation as 409 rather than 500
    - Fall back to the root path instead of throwing when a knowledge
      base's folder is archived
    - Cap bulk-download at the ceiling it actually enforces
    - Normalize variables through one helper on both graph write paths
    - Fix a folder-name log filter that matched workflow names
    
    Naming and gaps:
    - Rename /files/{id}/extract to /unarchive, /rows/find to /rows/search,
      /rows/batch-update to /rows/bulk-update, /columns/run to /dispatches
    - Type the last six generic [id] path segments
    - Add table folder restore and id-addressed dispatch cancel
    
    * chore(audits): record the v2 catalog routes in the boundary baseline
    
    * fix(api): accept a null chat password and correct three published claims
    
    - performChatDeploy validated `password: null` as a password, so the
      replace-shaped chat PUT answered 400 for every mode that owns no
      password — public (the default), email, and sso. The declared payload
      type has always allowed null, and the stored value is cleared by
      authType regardless, so null needs no validation of its own. The route
      test could not catch it: it mocks the orchestration module and pinned
      the exact null the real guard refused.
    - Redirect the two docs slugs this branch retired that were genuinely
      published: findTableRows and runTableColumns.
    - The table folder restore described an idempotent no-op for an already
      active folder; it answers 404. Say so, and say where the path comes
      from, since the tables folder list cannot yet report archived folders.
    - Name the customizations exception to the chat PUT's replace semantics.
    - A cursor-binding case used status=error, which is a level and not a
      status, so it failed contract validation and never reached the cursor
      check. Use an accepted value and pin the reason, not just the status.
    
    * chore(cli): classify the new v2 chat operation as non-destructive
    
    * feat(cli): expose canonical resource URLs
    
    * fix(api): close three caller-reachable failures found by the final probe
    
    - GET /files/{fileId}/text called parseBuffer unguarded, and parseBuffer
      signals every failure as a bare Error that no v2 policy classifies. A
      zero-byte upload or a mislabelled archive was an unhandled 500. Empty
      bytes now answer empty text — a zero-length file has no text — and
      unparseable bytes answer 409, matching the rendered-artifact resolver.
    - GET /workflows/{workflowId}/state asserted write-side bounds over
      stored data. workflow_blocks.name and .type are bare text() and the
      realtime rename op accepts z.string(), so a block renamed past 255
      characters made the workflow unreadable, and unrepairable, over v2.
      The read shape now takes the same input/stored split the variable
      schema already had. Stored subflow conditions are coerced in the
      loader beside the existing numeric guards.
    - GET /knowledge stamped scope into the cursor fingerprint
      unconditionally. scope defaults to active and is new on that list, so
      every cursor the deployed build handed out would have been refused
      with a message saying the caller changed a filter they never sent.
      Its siblings already carry the guard and the comment.
    
    Also: POST /workflow-mcp-servers answers 201 like every other v2 create;
    GET /logs/stats reuses the log list's entry ceilings; the execute and
    resume routes install the media-type-aware 415 they publish; and a
    rationale citing an endpoint that never reached the wire is corrected.
    
    * fix(tables): carry the dispatch terminal timestamps through the stale sweep
    
    Staging's abandoned-dispatch recovery builds its own `DispatchRow`, and
    this branch had added `completedAt`/`cancelledAt` to that shape for the
    id-addressed dispatch read and cancel. The merge was textually clean and
    left the new mapping short two fields.
    
    * fix(workflows): deny workspace API keys on the graph replace
    
    `PUT /workflows/{workflowId}/state` stores blocks and their tool wiring
    wholesale, but the policies deciding which of those a member may add —
    the EE permission config and block visibility — take a human subject.
    A workspace API key has none, and both substitutes fail open: the
    billing owner is a different, typically less-constrained person, and
    passing no user makes the permission lookup return null, which every
    caller reads as unrestricted.
    
    That made the replace a second graph-write door storing what its sibling
    `POST /workflows/{workflowId}/operations` refuses, which denies workspace
    keys for exactly this reason. Both doors now agree. Personal keys keep
    the capability, so headless authoring is unaffected for a credential
    that names a human.
    
    * chore(api): drop the enrichment catalog endpoint
    
    GET /api/v2/enrichments listed the code-defined table enrichments. The
    per-row enrichment run detail stays; only the catalog read goes.
    
    Removes the route, contract, response and query schemas, the semantic
    operation, the use case, the projection module, its registry-boundary
    entry, and the CLI command. The "not found" and "blank search" cases it
    covered are repointed at the connector-type sibling so the shared
    behaviour stays tested rather than deleted with it.
    
    * improvement(api): make the workflow operations endpoint self-describing
    
    Two things stood between this endpoint and a caller who has only the
    published spec.
    
    The accepted `params` keys existed only in the edit engine's source. The
    spec said "the accepted keys depend on the target block type", so a
    caller reading it could create a nameless empty block and nothing more —
    while the Copilot tool catalog, over the same engine, has always spelled
    out the envelope. That guidance now lives in the contract, shared by the
    add, edit, and insert_into_subflow parameter schemas so the two surfaces
    cannot describe one engine differently: `inputs` keyed by sub-block id,
    `retry`/`triggerMode`/`advancedMode` beside it rather than inside it,
    `connections` keyed by source handle, and `removeEdges` for dropping one
    edge without restating the rest.
    
    A `block_id` that is not already a UUID is replaced with a minted one,
    and the mapping was computed and then dropped. A caller could not
    reference the block it had just created except by re-reading the graph
    and matching on name. The engine now returns it and the response
    publishes it as `mintedBlockIds`, with the in-batch versus cross-request
    rule stated in the operation description.
    
    * fix(api): close the pre-merge scan's blocking findings
    
    Docs and public wire, all of it permanent surface once released.
    
    - Two published tag groups, Catalog and Meta, had no sidebar entry in any
      locale, so six operations shipped unbrowsable. Added to all six, and a
      test now fails when a published tag has no entry.
    - deleteWorkflowChatDeployment pointed callers at DELETE on /deployment;
      the undeploy verb is on /deploy.
    - PUT /state still promised workspace API keys a degraded lint pass after
      the operation began rejecting them outright. It also lived in a
      single-quoted string, so the shared clause would not have interpolated.
    - POST /tables/move took targetFolderPath as nullable-but-required, which
      rendered the CLI flag as a required `<json|@file>`: `--to /Archive`
      failed to parse and omitting it failed outright, while the docs said
      "omit for root". Now optional and a plain string, matching
      POST /files/move; the route supplies the null the use case wants.
    - Table dispatch status and row run state published `cancelled` beside
      imports, exports, and job state publishing `canceled`, and the note
      explaining the split was wrong about its own sibling. Both new schemas
      now publish `canceled`; the stored column is unchanged and mapped at
      the presenter. The shipped `cancelled` count field is left alone.
    - applyWorkflowVariables can answer 423 and both workflow-MCP deletes can
      answer 409; none…
    waleedlatif1 and TheodoreSpeaks authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    656840a View commit details
    Browse the repository at this point in the history
  12. feat(slack): launch v2 triggers and backfill custom bots (#6873)

    * feat(slack): launch v2 triggers and backfill custom bots
    
    * fix(slack): propagate legacy webhook dispatch failures
    
    * fix(slack): continue shared legacy webhook fanout
    
    * fix(slack): acknowledge filtered webhook deliveries
    
    * fix(slack): finalize custom bot migration rollout
    
    * fix(slack): dedupe migrated bots per workflow
    
    * fix(slack): harden custom bot rollout
    
    * fix(slack): acknowledge permanently ignored deliveries
    
    * fix(slack): retry failed webhook deliveries
    TheodoreSpeaks authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    4c0bd94 View commit details
    Browse the repository at this point in the history
  13. feat(integrations): add Semrush (#7068)

    * feat(integrations): add Semrush
    
    Adds the Semrush SEO API as a block with 44 operations across overview,
    domain, subdomain, URL, keyword, comparison, and backlink reports.
    
    Reports answer as delimited CSV, so the shared decoder maps each header
    cell back to the export column it was requested as rather than reading by
    position: the API may return fewer columns than were asked for, and two
    columns can render under the same header label.
    
    * fix(integrations): sync docs manifest for the Semrush page
    
    * fix(integrations): address Semrush review findings
    
    - Only the API key stays user-only; report selectors (target scope, limit,
      offset, date, sort, filter) are user-or-llm so an agent can set them
    - Hold the row limit at one row: a positive fraction floored to zero and sent
      display_limit=0
    - Locate Domain vs. Domain metric columns by their own headers, so a dropped
      position column shortens the compared-domain run instead of shifting
      competition, search volume, and CPC onto the wrong values
    - Describe competitor and domain-list metrics as belonging to the row's domain,
      not to the target
    - Describe paid and URL traffic cost as an estimated cost, matching the
      Traffic Cost header those reports return, not the organic Traffic Cost (%)
    - Drop the newest-first claim from history outputs, which order by display_sort
    - Replace the erased any casts in the request tests with a typed helper
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    ed60fba View commit details
    Browse the repository at this point in the history
  14. feat(integrations): add Microsoft Word (#7069)

    * feat(integrations): add Microsoft Word
    
    * fix(microsoft-word): guard document edits against concurrent overwrites
    
    * fix(microsoft-word): reject non-Word targets, scope SharePoint access, and tighten input bounds
    
    * chore(docs): regenerate docs manifest for the Microsoft Word page
    
    * fix(microsoft-word): strip XML-forbidden control characters from generated documents
    
    * fix(microsoft-word): fail closed when a document reports no version to compare
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    98a453d View commit details
    Browse the repository at this point in the history
  15. improvement(workflow): add compact code hover previews (#7074)

    * improvement(workflow): add compact code hover previews
    
    * fix(workflow): address code preview review feedback
    icecrasher321 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    b515fe0 View commit details
    Browse the repository at this point in the history
  16. Configuration menu
    Copy the full SHA
    e3b3b48 View commit details
    Browse the repository at this point in the history
  17. improvement(tools): tell the model which duplicate tool instance is w…

    …hich (#7079)
    
    An agent can hold two entries of the same tool bound to different resources, but
    they reach a provider byte-identical: user-filled params are stripped from the
    schema, and only id/description/parameters go on the wire. The second instance's
    opaque `__sim_2` alias carries no meaning, so the model picks between them
    arbitrarily.
    
    When two or more tools collapse to the same canonical id, each description now
    names what that instance is bound to — the OAuth account, knowledge base, or
    workflow. Single-instance tools are untouched and cost no extra lookup.
    
    Also removes ProviderToolConfig.name, which no provider ever sent, and the
    unreferenced createExecutionToolSchema.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    8de7ada View commit details
    Browse the repository at this point in the history
  18. fix(tables): prevent truncated sandbox mounts (#7075)

    * fix(tables): prevent truncated sandbox mounts
    
    * fix(tables): snapshot every sandbox mount
    
    * fix(tables): cap aggregate snapshot mounts
    
    * fix(tables): reject stale sandbox snapshots
    TheodoreSpeaks authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    470946d View commit details
    Browse the repository at this point in the history
  19. improvement(access-control): wire tool and model permissions into cop…

    …ilot editing (#7080)
    
    * improvement(access-control): wire tool and model permissions into copilot editing
    
    Permission groups already supported a per-tool denylist (deniedTools) and model
    restrictions, but only the canvas honored them. The copilot edit path gated on
    block type alone, so Sim could build workflows using tools and models the user
    was not allowed to run — the executor refused them at run time instead.
    
    Enforce both at authoring time, and stop advertising what the viewer cannot use.
    
    * fix(access-control): use the path alias for the permission-groups type import
    
    * fix(access-control): close two denied-tool leaks in copilot discovery
    
    The VFS stamped every integration schema from the shared static map before the
    per-viewer loop re-authored the permitted subset, so a denied operation's schema
    stayed published. Skip the shared copy for integration paths; the viewer loop is
    the only projection that knows the denylist.
    
    Block metadata resolved denied operations from the catalog's `operation.toolId`,
    which the projection fills only from `tools.config.tool` — a block whose
    operation ids are its tool ids left it undefined and read as fully permitted.
    Resolve through the shared operation gate instead.
    
    * fix(access-control): key the copilot schema cache on permission policy
    
    The deferred integration-tool schemas now depend on the viewer's permission
    group, but the cache key encoded only identity and block visibility, so an
    admin's change to deniedTools took effect only when the entry expired.
    
    Resolve the config before the key and add a gate signature alongside the
    existing visibility signature, mirroring how block visibility already keys the
    same cache. The read moves out of the cached section rather than being added:
    what the entry caches is a user-tool schema per exposed integration tool, which
    dominates it.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    32a15fd View commit details
    Browse the repository at this point in the history
  20. fix(copilot): name a count-parallel's branch count count in the mod…

    …el's read view (#7082)
    
    sanitizeForCopilot builds the workflow JSON the model reads before editing.
    For a loop it emits `iterations` from `block.data.count`, which is right: the
    loop write contract (components/blocks/loop.json) declares `iterations`, and
    the edit path reads `params.inputs.iterations` back into `data.count`.
    
    The parallel branch copied that line without renaming the field. But the
    parallel write contract (components/blocks/parallel.json) declares `count`,
    and the edit path reads `params.inputs.count` — so the model was shown a
    branch count under a name its own write contract does not define. Echoing it
    back drops the value silently: neither addSubflowConfig nor the update path
    looks for `iterations` on a parallel.
    
    Scoped to the copilot read/edit round-trip. sanitizeForExport is unaffected —
    it preserves state.blocks wholesale, so JSON export and folder backup/restore
    always carried the real data.count.
    
    The sanitizer had zero parallelType coverage, which is how this drifted. Adds
    three cases pinning both contracts and the collection variant; the parallel one
    fails against the old code.
    waleedlatif1 authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    ffe3ab1 View commit details
    Browse the repository at this point in the history
Loading