Commit Graph
339 Commits
Author SHA1 Message Date
Waleed f0b79c5cc6 chore(deps): upgrade next 16.2.11 -> 16.2.12 (#6077)
16.2.12 is the current stable (published 2026-07-25) and its entire
changelog is two PRs: a docs backport and vercel/next.js#95831, "Fixes to
support TypeScript 7".

That second one matters here. `apps/sim` declares `typescript: ^7.0.2` and
the lockfile resolves 7.0.2, while 16.2.11 predates any TS7 handling — not
even the actionable-error guard (#95837), which was never merged. The
upstream symptom is `next build` dying with a silent SIGSEGV during its
type-check step, because the legacy TypeScript JS API that Next called is
gone in TS7. Builds pass today only because Next detects
@typescript/native-preview as the compiler and takes a different path, so
we are accidentally-working rather than supported. 16.2.12 adds the
`experimental.useTypeScriptCli` backend that makes this configuration
official.

Zero build-performance content in the patch, so this is not a speed change.

Bumps all eight pins in lockstep — next, @next/env and the four
@next/swc-* binaries at the root, plus the three app/package copies. The
swc binaries must move with next: they are platform-gated
optionalDependencies, so a version skew or a gate exclusion leaves them out
of bun.lock entirely and `bun install --frozen-lockfile` installs no
compiler at all (the #5945 failure).

Also re-dates the bunfig.toml gate note, which said to drop the next
entries on 2026-07-28 — yesterday. 16.2.12 is inside the 7-day window until
2026-08-01, so following that instruction would have blocked this bump and
re-triggered the missing-compiler failure. Re-date on future bumps rather
than deleting the entries early.
2026-07-29 18:10:50 -07:00
1d64b92b41 feat(desktop): desktop app (#5998)
* top on a desk

* fix auth stuff

* intermediate state

* update

* local filesystem fixes

* Huge

* fix banner

* ci: disable desktop release + e2e in CI for now

The desktop-release reusable-workflow call requested contents: write,
which ci.yml's permission grant (contents: read) rejects — invalidating
the whole CI workflow. Desktop is tested locally for now; signed builds
remain available manually via desktop-release.yml workflow_dispatch, and
desktop e2e via its own workflow_dispatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: exempt electron from the release-age gate (time-boxed)

electron@43.1.1 (published 2026-07-14) is exact-pinned for the desktop
shell and blocked by minimumReleaseAge until 2026-07-21. Excluded with a
drop-after date, following the vetted-typescript precedent. Verified the
rest of the desktop dependency set clears the 7-day gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* desktop: brand app icon (packaged + dev Dock)

- build/icon.icns regenerated from public/logo/primary/large.png on the
  Apple icon grid (824px body, r=185.4, centered on a transparent 1024
  canvas), compiled with iconutil
- dev runs set the same mark via app.dock.setIcon (static/dock-icon.png) —
  unpackaged Electron otherwise shows its default atom icon
- un-ignore apps/desktop/build: it holds electron-builder INPUTS (icon,
  entitlements), which the /apps/**/build output rule was swallowing —
  the icns and entitlements were never actually tracked
- revert resetAdHocDarwinSignature fuse: it corrupts the packaged binary
  signature (app killed at launch on arm64); the local ad-hoc deep-sign
  flow doesn't need it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* desktop: switch app icon to the b&w brand mark

White rounded tile with the black sim wordmark (from
public/logo/b&w/large.png), replacing the purple variant. Same Apple
icon grid geometry (824px body, r=185.4, 1024 canvas).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix banner

* Fix

* clean up launcher

* fix oauth

* update desktop app

* Improve browser use and consolidate desktop app

* Desktop app ui cleanup

* Updates

* Updates

* remove dev tool option

* Browser updates

* Fix electron bug

* Browser shortcuts

* lifecycle

* feat(desktop): SSRF hardening + shared @sim/security/ssrf (re-home of #5763) (#5784)

* feat: re-home @sim/security/ssrf + sim SSRF dedup onto dev (clean core)

* feat(desktop): re-integrate SSRF guard + hardening onto rewritten dev

Re-applies the browser-agent SSRF guard and hardening onto dev's evolved
desktop files (dev rewrote session/driver/handoff/index and split out
errors.ts/keyboard.ts):

- session.ts: agent-partition onBeforeRequest is the SSRF choke point —
  DNS-resolving check (fail-closed) for document navigations, synchronous
  literal-IP backstop for subresources.
- driver.ts: browser_navigate/browser_open_tab validate via checkAgentUrl for a
  clean model error; also adopt shared sleep/getErrorMessage and drop the local
  reimplementations + banner separators.
- index.ts: local-only crashReporter (native minidumps, no upload) + CSP
  fallback wired into the app session.
- window.ts: record the crash-dump dir on renderer_gone.
- config.ts: drop the local LOCAL_HOSTNAMES set for the shared isLoopbackHostname
  (also removes the dead bare '::1').
- cdp.ts: per-WebContents callbacks so a background tab's events reach its own
  driver.
- updater.ts: the manual check now surfaces network/manifest failures instead of
  silently swallowing them.
- README: correct the App Sandbox / security-scoped-bookmark note.
- electron-mock: webRequest.onBeforeRequest + crashReporter stubs.
- api-validation: annotate dev's validated-envelope double-cast; bump the
  route-count baseline 964→965 for dev's already-merged route (ratchets stay
  tight; non-Zod and double-cast at baseline).

Skipped as moot (dev already did them independently): launcher isVisible removal,
decideStartRoute param drop, local-filesystem clear() removal.

* chore(desktop): biome format install-local.ts (pre-existing dev lint failure)

* refactor: apply audit cleanup (reuse + simplify)

- domain-check: drop the redundant isIpLiteral guard (isLoopbackIp already
  validates and returns false for non-literals).
- session.ts: use shared getErrorMessage instead of the local error ternary
  (the file already imports it).
- tray.ts: use shared sleep() instead of a hand-rolled setTimeout promise.
- updater.ts: distinguish the synchronous-throw log from the async-rejection
  log on the manual update check.

* refactor: /simplify pass + review fixes

- url-guard: bound the SSRF dns.lookup with a 5s deadline (fails closed on
  timeout) so a slow/hung resolver can't suspend the check and the
  onBeforeRequest callback indefinitely (Greptile P2); + test.
- Finish the reuse consolidation the earlier pass missed: session.ts second
  error ternary → getErrorMessage; the bracket-strip idiom → unwrapIpv6Brackets
  in input-validation.ts, input-validation.server.ts (×2), onepassword/utils.ts
  (fixes the check:utils banned-pattern CI failure).
- driver: document why the tool-level checkAgentUrl coexists with the
  onBeforeRequest enforcement seam (clean model error; loadURL rejection is
  swallowed).

* fix(desktop): swallow late DNS rejection after the SSRF lookup timeout (Cursor)

* refactor: split pure host helpers into @sim/security/hostnames (ipaddr-free) (#5787)

unwrapIpv6Brackets + isLoopbackHostname move to a new ipaddr-free sub-export so
client code can share them without pulling ipaddr.js into the browser bundle.
ssrf.ts re-exports both, so its server/desktop consumers are unchanged. This
eliminates the duplicate isLoopbackHostname in apps/sim/lib/core/utils/urls.ts:
urls.ts and its three client importers (mcp queries, oauth probe, oauth
url-validation) now use the single shared definition.

* Desktop app fullscreen mode

* fix(copilot): report closed browser session as a distinct terminal tool error

A dead agent browser session used to answer every browser tool with an
indistinguishable generic ~30s IPC timeout, which the model retried
indefinitely (one turn: 59 minutes of failing browser_snapshot calls).

- When the desktop app has reported the session closed, page-dependent
  browser tools fail immediately with an explicit session-closed message
  (and sessionClosed: true in the result data) instead of burning the full
  timeout per call. browser_navigate / browser_open_tab / browser_list_tabs
  still run, since they can start a new session.
- A failure whose session died mid-call (e.g. during a takeover) gets the
  same tag appended, so the model learns the terminal cause rather than
  seeing a plain timeout.

Companion to mothership's tool_failure_loop circuit breaker.

* fix(desktop): route Cmd+W to focused browser tabs

* fix(desktop): reserve macOS title bar safe area

* fix(desktop): limit title bar safe area to login

* fix install script

* feat(desktop): improve local folder settings

* feat(desktop): harden local capabilities and window chrome

* fix(invitations): live refetches

* fix(desktop): make manual update checks use updater state

* fix(desktop): review fixes — OAuth error handling, query freshness, invitations

Findings from an end-to-end review of the desktop work, fixed and verified.

OAuth connect/login handoff:
- Add a friendly /oauth-error landing page + onAPIError.errorURL so provider
  Cancel/Deny (which Better Auth redirects before the flow state is parsed)
  no longer dead-ends on a 404; re-initiating supersedes the idle loopback.
- Stop a post-consent failure from reporting success (drop the baked-in
  errorCallbackURL param that collided with Better Auth's appended code;
  coerce an array error defensively on the complete page).
- Guard the desktop connect listener with the same context-age check the web
  routers use, so an abandoned flow can't mislabel a later completion.
- Clear an orphaned pending handoff when a loopback re-bind fails.

Query freshness (desktop refetchOnWindowFocus):
- Pin refetchOnWindowFocus off on queries that seed editable forms
  (environment/secrets, credential detail, schedules) so a background focus
  refetch can't drop an unsaved draft, and on the useWorkflowStates fan-out so
  returning to a large table doesn't fire N heavy envelope fetches. All no-ops
  on web (default already false).

Invitations (in-app pending invitations):
- Map accept/decline failures to friendly copy instead of raw machine codes.
- Invalidate subscription + refresh session on accept (parity with the email
  path); reconcile the list on failure (onSettled) so dead rows drop.
- Gate the modal's query on open so it no longer fetches on every app load.

CI:
- Wrap the latest-mac.yml update-feed route in withRouteHandler and allowlist
  it as a non-boundary route (input-less, YAML) so the contract audit passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* updates

* fix(desktop): use workflow colors for environment icons

* fix(desktop): use orange for dev icon border

* fix(login): change one time token generation to GET

* improvement(desktop): reveal local folders from settings

Local-folder rows rendered their glyph at 20px inside the bordered
credential tile — chrome meant for brand and logo icons — above a static
subtitle that repeated what the section already said. The row now shows a
plain 14px folder icon and the folder name alone.

Clicking a row reveals the folder in the OS file manager through a new
reveal_mount bridge op, which resolves the opaque localfs URI to a live
grant and requires an active user gesture, matching the other grant
mutations. The absolute host path still never crosses the bridge.

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

* improvement(desktop): row actions menu for folder grants, larger version text

Revoke moves from an always-visible chip into the canonical RowActionsMenu,
matching the MCP server rows. The version value moves off text-caption onto
text-sm — it was rendering at the subtitle size, which also shrank the
"x -> y on restart" line that matters most.

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

* feat(desktop): improve browser tab usability

* fix(desktop): thicken environment icon borders

* fix(desktop): strengthen environment icon borders

* feat(desktop): support multiple windows and harden the agent browser

Sim can now open many full windows in one process. The embedded browser is
still a single native surface, so exactly one window owns it at a time.
Ownership transfers only to the focused window: without that rule, two windows
both showing the browser reclaim it on every bounds heartbeat and re-parent the
native view back and forth roughly once a second while Sim sits in the
background, where no window is focused. A destroyed owner is now forgotten
rather than left rejecting updates from the window actually on screen, and a
closing window's release is honoured even though Electron destroys it before
emitting `closed` — previously that release was dropped and the next layout
could re-parent the browser onto a window that never asked for it.

The agent's password boundary is now enforced rather than assumed. It was
treated as settled but had four ways through: `browser_press_key` sent trusted
CDP keystrokes to whatever held focus, `clickElement` focused credential fields,
`readActiveElementState` returned a preview of any focused value, and snapshots
printed the contents of revealed password fields. Detection also used
`instanceof HTMLInputElement`, which is realm-bound and returned false for
inputs inside same-origin iframes — the nested login forms that need it most.
Detection now matches on tagName/type/autocomplete, the keystroke guard runs in
the driver where trusted CDP input is visible, and typing re-checks the real
target before inserting, since login forms advance focus between the username
and password steps.

Signing out clears the embedded browser's profile. Its cookies, cache, pinned
tabs, browsing trail, and reopen list all survived sign-out, so the next account
on the machine inherited the previous user's live sessions.

Partition hardening is keyed per session instead of a process-wide flag, which
would have left a second partition with no permission handlers, no SSRF
filtering, and no download blocking — silently, and still type-checking.

Adds the first tests for page-functions.ts, including a serialization contract
check: those functions ship to the page as String(fn), so a reference to module
scope passes every other test and fails only against a real page.

* fix(desktop): close clipboard, glob DoS, and authorization holes

Found by a full audit of the desktop app against origin/staging. Each of
these was measured or asserted rather than reasoned about.

The agent could read the user's system clipboard. `browser_press_key('Cmd+V')`
pasted it into a focused field and the next `browser_snapshot` returned it as
an ordinary `value` — snapshots redact password fields, not pasted content, and
clipboards routinely hold a password copied out of a manager. The credential
guard added earlier did not catch it: `insertedTextFor` returns undefined
whenever `meta` is set, so `Cmd+V` was classified as not text-inserting.
`Control+V` reached the same place because the macOS normalizer rewrites it.
Clipboard combos are now refused before dispatch rather than by withholding the
CDP `commands` array, since off macOS these are Blink-native and a key event
alone still performs them. Copy and cut go too — they clobber the user's
clipboard as a side effect.

A glob pattern could freeze the whole app. Micromatch compiles to a
backtracking regex whose cost is exponential in wildcard count: measured
against a single 46-character path with the options this code passes, ten
wildcards took 2.7s and twelve took 43s, once per scanned entry, in one
synchronous call that the surrounding abort checks never get to interrupt. That
is the main process, so every window, the menu bar and the tray freeze with
Force Quit as the only recourse, and the pattern is model-supplied. `safeRegex`
reports the generated source as safe, so it was no defense. Patterns are now
bounded at six wildcards, which keeps the worst case near 2ms while leaving
headroom over real patterns (which top out around four). A timing probe backs
it up, with a budget loose enough that JIT warmth and machine load cannot make
it fire on a legitimate pattern — a tight budget proved flaky in both
directions.

The grep authorization guard compared `request.pattern !== args.pattern`, so a
tool call carrying no pattern made that `undefined !== undefined` and the guard
passed — grep then fell back to searching the renderer's own `query` across the
whole grant. `include` and `query` were never bound at all, letting a renderer
widen a search or silently narrow results the agent believes are complete. The
sibling glob case already had the `typeof` check, which is what made the
asymmetry clearly unintentional.

The IPC sender gate used `startsWith`, the exact pattern `isAppOrigin` warns
against 200 lines away ("that prefix-matches lookalike hosts"). It was safe only
because of a trailing slash. It now uses that helper, which also fixes a false
negative on an explicitly stated default port.

* fix(desktop): stop double sign-out, stranded retries, and redundant writes

Three correctness bugs from the same audit.

Menu Sign Out tore down the session directly instead of going through the
lifecycle coordinator, so it skipped the in-progress guard — and its own cookie
removal then tripped the coordinator's cookie watcher into a second concurrent
teardown, duplicating the sign_out event, the storage clear, and the /login
load. Teardown also existed as two divergent copies. The coordinator now
exposes `signOut()` and owns the single path; the menu just calls it. That
`tearDownSession` is no longer imported in index.ts is the check that it landed.

Offline recovery could strand permanently. The auto-retry loop stops itself
before calling `retry()`, and `retry()` never re-armed the load watchdog, which
is started once per window. So if a retried load hung — precisely what the
watchdog is for — no load event fired and no timer remained anywhere; the user
sat on the offline page until the window was closed. `retry()` now re-arms
before loading.

Pinned tabs were persisted on `did-navigate` and `did-navigate-in-page` for
every tab, pinned or not, with no change check, and the settings store compares
with `===` so a freshly built array never matched. Any single-page app
therefore triggered a synchronous mkdir + write + rename of the whole settings
file on the main thread on every route change — writing `[]` over `[]` when
nothing was pinned. The list is now fingerprinted, seeded at restore from what
is already on disk so the first navigation after launch is not a write either.

* fix(desktop): leaked timers, silent grep failures, and crashed tabs

Second pass on the audit backlog, all verified against tests that fail without
the change.

Every browser tool call leaked a timer. The watchdog raced the tool against
`sleep()`, which cannot be cancelled, so when the tool won — the normal case —
the timer stayed pending for the full window, up to two minutes, dozens deep
during an agent run. Replaced with a cancellable timeout cleared in a
`finally`; a test asserts the fake-timer count is unchanged across a call.

An invalid grep regex reported "no matches". A SyntaxError from `new RegExp`
returned an empty result set, which tells the model the string appears nowhere
in the user's files — a factual claim it acts on, when the search never ran. It
now fails as INVALID_REQUEST. The `safeRegex` guard moved out of the try while
there, since it was only inside it to be re-thrown.

A crashed tab wedged the session. Tabs left `tabs` only via close, so a dead
renderer stayed forever: `activeTab()` filtered it out while `activeTabId` still
named it, making `requireTab()` report "no page is open" with other tabs open,
and the panel went blank with no recovery. `render-process-gone` now drops the
tab, advances the active id, and reports session closure when it was the last.

`probeSession` cleared its abort timer inline after the await, so a thrown
fetch — the case the function exists for — skipped it. Moved to `finally`,
which also brings the body read inside the deadline.

One vanished file failed a whole directory listing: `Promise.all` over
per-entry `lstat` turned a single ENOENT into NOT_FOUND for the directory.
Churning directories like build output would intermittently fail to list.

Removed the `session-lifecycle -> browser-agent/driver` import edge, which
dragged the entire browser subsystem and its module-load `nativeTheme` listener
into the auth path to reach one four-line function. `clearBrowserProfile` is now
a required dependency wired from index.ts, which already owns both sides. Also
deleted `attachSessionLifecycle`, a compatibility wrapper with zero callers.

Added a channel-parity test between the preload bridge and the IPC table. They
share ~20 channel names as bare string literals with nothing tying them
together, so a typo on either side is a silently dead feature that type-checks
and ships. Verified it fails on a one-character change.

* fix(desktop): reach framed elements and harden the loopback sign-in

Two behaviour fixes from the audit backlog.

Interaction with same-origin iframes was broken. The snapshot deliberately
walks into those frames and hands the model ids for what it finds, but every
interaction then tested `instanceof HTMLInputElement` against the top frame's
constructors — false for nodes owned by a frame, because element wrappers are
realm-bound. So the driver reported a real `<input>` as "not a text input",
which took out framed login forms and editors that put a contenteditable body
in an iframe, such as TinyMCE. Framed selects reported "not a select" and
framed clicks skipped focus entirely. Checks now compare `tagName` or duck-type
the method being called, matching the realm-safe approach the credential guard
already used. The native value setter is taken from the element's own realm:
calling the top frame's setter on a frame's node throws "Illegal invocation".
Snapshot value reporting follows the same rule, which is safe because the
credential redaction above it is realm-safe and runs first.

The loopback sign-in server could be cancelled by anything on the machine. It
validated only the shape of the returned state, then tore the one-shot server
down and dispatched, leaving the real constant-time comparison to the callback.
So a request carrying any well-formed state killed an in-flight sign-in — and
the port is reachable by any local process and by any page the user has open
via a no-CORS GET, which cannot read the response but does not need to, since
the side effect is the kill. The state is now checked before anything is torn
down, and a Host that does not name the loopback is refused, which closes the
DNS-rebinding shape.

* refactor(desktop): drop duplicated helpers and stop logging query strings

Net -3 lines, and one of them was a real leak.

`navigation.ts` and `windows.ts` truncated URLs for their log lines with a bare
`.slice(0, 200)`, which keeps the query string — the five other log sites in the
app go through `scrubUrl` for exactly that reason. Tokens and signed parameters
live in query strings, so a blocked-URL warning could write one to disk. Both
now scrub.

`local-filesystem.ts` carried a private `isRecord` byte-identical to
`isRecordLike` in `@sim/utils/object`, and four more sites inlined the same
check. All now use the shared helper, which also tightens three of them: the
inline versions omitted the array exclusion, so an array satisfied a check that
then cast it to a record.

`tray.ts` hand-rolled slice-plus-ellipsis, the case `@sim/utils/string`'s
`truncate` exists for. Titles between 58 and 60 characters now get an ellipsis
where they previously did not — cosmetic, in a tray menu label.

Removed the `getTabsState` passthrough in the driver, a one-line re-export of
the session's own function, and renamed the session-level clear to
`clearProfileStorage`. `clearBrowserProfile` existed twice under one name, the
driver's being the composite that also clears the browsing-trail registry;
index.ts was already aliasing at the import to tell them apart.

Two things deliberately not done. The hand-rolled semver in updater.ts stays:
replacing it needs `semver` plus `@types/semver` as new declared dependencies
in the Electron main process, and the 90 lines it would delete are already
covered by eight assertions that I verified match the library's behaviour case
for case. Note the same prerelease comparison is duplicated in
apps/sim/lib/desktop/min-version.ts, so a future consolidation should do both.
No barrel for browser-agent either: routing `security-guards.ts` through one to
reach a single leaf function would pull the whole browser subsystem into its
module graph, which is the edge just removed from session-lifecycle.

* refactor(desktop): move browser compositing out of the session module

session.ts held five responsibilities in one flat namespace: 1,061 lines, 29
exports, 26 mutable module-level bindings. For contrast local-filesystem.ts is
a comparable 1,125 lines with two exports and no ambient state — size was never
the problem, the shared mutable namespace was.

Compositing is the part worth isolating. Where the native view sits, when it is
visible, which window owns it, the renderer bounds lease, and the occlusion
snapshot are the most intricate logic in the browser and are almost entirely
separable from tab bookkeeping. They now live in panel.ts (342 lines) and
session.ts is 792, with 15 bindings instead of 26.

The two modules were mutually dependent, which is what makes this kind of split
go wrong. Rather than events or a shared store, panel.ts takes the four things
it needs from the session through one PanelHost passed to initPanel — the same
shape as the existing initSession — so the import graph is one-way and there is
no new indirection to trace. Tab changes reach the panel by the session calling
layout(), exactly as before.

Two behaviours became explicit rather than implicit in the move:
detachIfAttached replaces callers reading `attachedView` to decide whether a
closing tab owns the surface, and isPanelVisible replaces `panelBounds !== null`.

Nothing about the split is verified by the split itself, so the bounds lease got
characterization tests first. It had none — there was not a single fake timer in
the suite — despite being the mechanism that hides the view when the renderer
crashes or wedges. Both tests were confirmed to fail against a broken lease
before the refactor began. The other 47 tests were not rewritten: only the
module their calls address changed, which is the useful signal that behaviour
was preserved.

Deliberately not split further. Focus tracking stays with tabs because it keys
off tab ids, and profile teardown stays put; separating either would be
taxonomy rather than decoupling.

* refactor: drop the legacy local_* filesystem tool shim

Granted folders are addressed through the ordinary VFS: the model calls
read/grep/glob against paths under user-local/, exactly as it does for
workspace files. A parallel local_read / local_grep / local_glob / local_list /
local_stat / local_mount_directory / local_list_mounts / local_forget_mount /
local_stage_file toolset existed alongside it, recognized but never advertised,
so an in-flight checkpoint written by an older desktop build could still finish.

There are no older desktop builds. apps/desktop is at version 0.0.0, the only
artifacts are a local 0.0.0 build, MIN_DESKTOP_VERSION is '0.0.0' meaning no
floor, and the app does not exist on staging at all — the v0.7.x tags are the
web app's. Nothing can have persisted a checkpoint naming these tools, and
nothing advertises them: they are absent from the generated tool catalog and
from mothership's catalog. The shim was defending against a past that never
happened.

Removes the name table, the legacy request builder, the server-side
LEGACY_READ_ONLY_TOOLS allowlist, the five local_* branches in the desktop
authorization switch, and nine display labels. isDesktopFilesystemToolCall
collapsed into isUserLocalVfsToolCall, which it had become a synonym for.

Two tests went with it. One asserted that local_list_mounts routes to the
desktop; the test immediately after it already covers the real path, an
ordinary read against a user-local path. The other asserted that legacy names
cannot open a folder picker, revoke a grant, or upload bytes — that property
now holds because no such tool name exists, which is a stronger guarantee than
refusing one.

* refactor(copilot): remove the plan/changelog VFS artifacts and workflow aliases

These beta surfaces are not a direction we are taking, so they come out rather
than staying behind a flag. Gone: the workflow alias modules (path resolution,
DB-backed resolver, .plans/.changelogs backing provisioning), the alias
materialization in the copilot VFS, the alias write paths in resource-writer
and workspace_file, the sandbox alias mounts in function_execute, the reserved
backing-path guards across mkdir/mv/create, and the alias resolution in the
chat home file picker.

xlsx survives but changes owner. It was gated twice across the repo boundary:
mothership's xlsx-writing flag gates the skill and prompt, while Sim gated the
compile path on mothership-beta. Those live in separate AppConfig applications,
so an operator had to flip two flags in two consoles, and off-hosted Sim fell
back to the MOTHERSHIP_BETA_FEATURES secret while the mothership half stayed in
Sim Cloud's AppConfig — split-brain across an ownership boundary. Mothership
controls whether the model ever learns xlsx exists, so if it is never offered
it is never requested and the second chokepoint only created a way for the two
halves to disagree. Sim's gate is removed; xlsx-writing is now the single owner.

With its last consumer gone, the mothership-beta flag and the
MOTHERSHIP_BETA_FEATURES secret are deleted. The two entries in the infra repo
are harmless until removed separately: they only inject an env var nothing
reads, and createEnv runs with skipValidation.

The reserved-system-file/folder concept goes with the aliases, since it existed
only to hide the backing rows. includeReservedSystemFiles and
includeReservedSystemFolders are removed rather than left as options every
caller passes true to. backingVfsPath is removed for the same reason — nothing
sets it once aliases are gone, so it was an always-undefined field on tool
results.

Test coverage is preserved rather than deleted with the feature.
resource-writer.test.ts looked alias-only but three of its eleven cases cover
the generic create path that survives; those are kept and the file retitled.
Two open_resource tests and one output-path test used alias-shaped strings
while asserting generic behavior; retargeted or dropped where a sibling already
covers it.

* refactor(copilot): remove the dead planArtifact column plumbing

copilot_chats.plan_artifact has no writer and no reader that does anything with
it. No client sends it, nothing renders it, and its whole history is fork-chat
and duplicate-chat plumbing faithfully copying a column that is always null —
the one change that might have populated it (mothership v0.8) was reverted.

Removed from the schema, the copilot API contract, the chat lifecycle column
sets, the fork route, superuser import, the data drain, the update-messages
write path, and the legacy chat detail response.

No migration here on purpose. The column stays in the database, orphaned and
null; dropping it is a separate deliberate step rather than something that
rides along with a code cleanup. Note that the next drizzle-kit generate will
now want to emit the DROP COLUMN, and check-migrations-safety will ask for it
to be annotated — that is the right moment to decide, not now.

Mothership never saw this field; it is Sim-side only.

* chore(copilot): sync the tool catalog for load_skill

Picks up the new load_skill tool plus the grep description that dropped its
stale reference to VFS "plans" entries. Generated from
copilot/contracts/tool-catalog-v1.json.

* refactor(copilot): follow the load_custom_tool rename to load_mcp_tool

Mothership renamed the loader once it was clear MCP was the only catalog kind
it could match, and dropped the single-valued `type` parameter. The two prompt
strings that teach the model the call shape are updated to
load_mcp_tool({ name }).

load_custom_tool stays in the UI hide-list next to load_agent_skill so tool
rows in historical transcripts keep rendering; nothing emits it any more.

* chore(copilot): sync the tool catalog and hide load_skill in the UI

load_integration_tool and list_integration_tools now publish route go/sync
instead of sim/async. Nothing changes in Sim's behavior — they always ran in
Go; the contract had been wrong.

load_skill joins the hidden tools. It is the same shape as the other loaders
already there: the agent pulling in a reference guide before doing the work is
a step toward the action, not the action. Sim's display-coverage test caught
that a newly added visible tool had no title or completed verb, which is the
guard working.

* fix(auth): handle session expiry in the app, not the desktop shell

The workspace auth gate is a Server Component, so it only re-evaluates on a
server render. A session that expired or was revoked mid-visit left the SPA
mounted and silently 401ing every request, with nothing to redirect it.

The desktop shell had grown its own detector for this: a 401 listener over
/api/*, a session probe, and a native "your session has expired" prompt. It
could only infer session state from cookie events and HTTP statuses, and it
inferred wrong — it fired on ordinary sign-outs (in-flight requests 401 during
teardown) and on launching already signed out (the window still shows the
restored route while the web app redirects). Those were nearly all of its
firings, since a 30-day sliding window means real expiry is rare.

Generalizes the impersonation-expired screen instead, which already had the
right shape: it keys off the session query settling to null after a session
that was live. A signed-out visitor never arms it, and `error` is excluded so
an offline blip cannot read as an expiry. The session query now refetches on
focus for every session, not just impersonation ones, so returning to a window
that slept through its session re-checks it.

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

* fix(copilot): port the scheduled-task and VFS fixes onto staging-v4

Replays the sim-side prompt-audit work on top of staging-v4.

complete_scheduled_task was filtered out of the execute route's response
payload, so an until_complete job could report completion and still be
rescheduled; the post-run bookkeeping now also refuses to revive a job that
already completed. Also clamps browser_wait_for's timeout the way the desktop
agent does, and replaces the oversized-read error's offset/limit advice, which
sent the model into a guaranteed retry loop.

* feat(desktop): let the model actually see browser screenshots

browser_screenshot captured an image and then threw it away. The renderer
stripped the data URL and substituted a note, and the tool's own description
told the model not to bother: "Dead end for perception." So the agent was
blind to anything not expressible as DOM text — canvas, charts, maps, images,
rendering and layout bugs.

The copilot has carried the machinery for this all along. A tool result shaped
as { content, attachment: { type: "image", source: { type: "base64", ... } } }
is serialized into a real image content block, with the media type sniffed from
the bytes rather than trusted from the declaration, and degraded to a text stub
when the routed model has no vision so the provider never 400s. The screenshot
result is now reshaped into that contract instead of discarded. A malformed
data URL still falls back to a note rather than shipping an attachment the
provider would reject.

Captures are bounded to a 1024px longest edge at quality 70. CDP clip.scale is
relative to CSS pixels, so this also sidesteps the device pixel ratio — an
unclipped capture on a retina display returns a 2x image, which was several
hundred kilobytes for no legibility the model could use.

The description is rewritten to bias toward visual questions only: appearance,
layout, rendering, charts, canvas. Reading content or finding something to
click stays with browser_snapshot, which is cheaper and returns the element ids
a screenshot cannot. That distinction is structural, not just advisory — having
seen the page does not let the agent act on it.

Companion change in mothership generalizes the tool-result inline-budget
exemption from "the read tool" to "any result carrying a model attachment".
Keyed on the tool name, an oversized screenshot fell through to the artifact
branch: the image was replaced by a reference the model cannot open, and the
result still reported success. Silent, and it would have hit almost every call.

* fix(desktop): polish browser panel and environment tray icon

* fix(desktop): enlarge environment tray markers

* fix(desktop): smooth environment tray markers

* refactor(copilot): consolidate resource mutation tools

* chore(copilot): clean up VFS follow-ups

* fix(desktop): round the dev tray marker

* feat(desktop): add integrated terminal resources

* Fix electron app resize causing glitchy browser frames

* feat(copilot): add persistent tool permissions

* fix(copilot): retire stale tool permission prompts

* fix(desktop): keep terminal rendering responsive

* fix(desktop): preserve resource rendering continuity

* feat(desktop): add browser tab duplication actions

* feat(desktop): add terminal tab context actions

* fix(desktop): allow browser agent localhost navigation

* feat(desktop): add tmux-backed terminal sessions

* fix(desktop): restore terminal scrollback per view

* chore(copilot): sync updated wait tool contract

* poll terminal session state for non regular shells

* add terminal right click menu

* feat(desktop): add terminal handoff and key batching

* fix(desktop): reserve the traffic-light lane from the platform

macOS draws the window controls itself, at a fixed physical size, above all web
content. The page renders full-bleed beneath them, so it has to reserve that
lane — and it did so with five hardcoded CSS pixel values. CSS pixels scale with
page zoom and the OS-drawn lights do not, so zooming out shrank the reservation
until the lights were drawn over the sidebar toggle, and the header row below
sat inside their band.

Electron's `titleBarOverlay` publishes the controls' real geometry to the page as
the `titlebar-area-*` env vars, which Chromium rescales per zoom so a reservation
derived from them holds its physical size. Measured across zoom 0.58-1.2, the
reserved area stays within ~0.6 DIP, the residual coming from env values being
quantized to whole CSS pixels.

Every lane length now derives from those vars, so the login route and the
mothership content offset were fixed without being touched — they already read
`--desktop-title-bar-height`. Two of the replaced constants were also simply
wrong: the platform reports the lane at 38px and the safe area at 81px, against
the hand-measured 36 and 83.

The toggle keeps a constant physical size beside the lights, expressed as a
proportion of the lane rather than in pixels: a px literal would scale with zoom,
and calc cannot divide a length by a length to recover a scale factor.

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

* fix(desktop): avoid transient terminal tab labels

* feat(copilot): attach browser and terminal tab context

* feat(desktop): close tmux panes from terminal tools

* fix(desktop): keep terminal tab icons stable

* add right click to browser and cleanup terminal right click options

* fix(desktop): reduce hidden panel background work

* perf(desktop): shrink browser panel snapshots

* perf(desktop): reduce terminal main process overhead

* perf(terminal): pause work for hidden sessions

* fix session arch for desktop

* fix(desktop): replace exited terminal sessions

* feat(copilot): persist desktop resources across chats

* fix(emcn): keep resource tab widths consistent

* fix(copilot): restore active client panels

* feat(desktop): import Chrome browser data

* fix(copilot): close resources before chat creation

* feat(desktop): suggest imported browser sites

* fix(desktop): autofill identifier-first sign-ins

* fix resizing issues + cookies source

* fix visits marking

* chore(db): drop branch migrations ahead of staging merge

0264/0265 on this branch collide with staging's 0264-0270 on both the
journal idx slots and the meta snapshot filenames. Reverting the migration
artifacts to the merge-base lets staging's chain merge cleanly; schema.ts
keeps the copilot changes and drizzle-kit regenerates a single migration on
top of 0270 after the merge.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(db): regenerate copilot tool-permission migration on top of staging

Replaces the branch's old 0264/0265 (dropped pre-merge so staging's
0264-0270 chain could apply cleanly) with a single 0271 generated against
staging's schema: the permission-decision enum, the two
copilot_async_tool_calls decision columns, and copilot_chats.auto_allowed_tools.

Deliberately does NOT drop copilot_chats.plan_artifact. The branch removed
every reader, but the currently-deployed code still SELECTs that column, so
dropping it in the same deploy breaks the old app version during blue/green
overlap — `check:migrations` flags it for exactly this reason, and the honest
fix is to defer rather than annotate around it. The column is retained in
schema.ts marked @deprecated; drop it in a follow-up once this has rolled out.

Also in this commit, all fallout from the merge itself:
- pinned-fetch/revoke tests: their private-IP stub moved to @sim/security/ssrf
  alongside the source change. Worth noting the stub exists because the suite's
  203.0.113.10 is TEST-NET-3, which the real classifier correctly calls
  reserved — the old stub had been quietly disagreeing with production.
- materialize-file test: dropped the reserved-system-folder case, which covered
  the workflow-alias backing folders this branch deleted.
- api-validation route ratchet 977 -> 983 (this branch's new routes).

Co-Authored-By: Claude <noreply@anthropic.com>

* add cmd f

* review pass

* chore(db): drop branch migration ahead of staging merge

Both sides independently claimed idx 0271, so the snapshot and journal would
conflict add/add. Ours is plain additive DDL that drizzle regenerates from
schema.ts; staging's is a hand-written CONCURRENTLY index build that cannot be
regenerated. Dropping ours and re-generating on top of staging's is the only
order that preserves both.

schema.ts is deliberately untouched — it is the source of the regeneration.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(db): drop branch migration ahead of staging merge

Both sides independently claimed idx 0272, so the snapshot and journal would
conflict add/add. Ours is plain additive DDL (one enum, two columns, one jsonb
default) that drizzle regenerates from schema.ts; staging's is a hand-written
migration with DO blocks and CONCURRENTLY index builds that cannot be
regenerated. Dropping ours and re-generating on top of staging's is the only
order that preserves both.

schema.ts is deliberately untouched — it is the source of the regeneration.

Co-Authored-By: Claude <noreply@anthropic.com>

* style(db): biome-format the regenerated migration metadata

drizzle-kit emits _journal.json and the snapshot with expanded arrays, which
biome check rejects. The merge commit used --no-verify, so lint-staged never
formatted them and CI's lint step failed on exactly these two files.

Whitespace only — both files are byte-identical under `jq -S -c`.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(desktop): pin the platform in the OS-auth tests

promptForSecret gates Touch ID on process.platform === 'darwin'. The suite
mocked electron's systemPreferences but inherited the runner's real platform,
so the eight biometric expectations passed on a Mac and failed on Linux CI,
where every call fell through to the confirmation dialog instead.

Pins the platform per-test and restores it after, and adds a case for the gate
itself — the branch whose absence from the suite is what let this through.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(desktop): refine environment dock icons

* fix(desktop): align packaged environment icons

* fix(desktop): keep packaged dock icon rendering consistent

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-07-28 19:25:59 -07:00
Waleed 665fd4ebeb chore(deps): vendor the free-email domain list and drop unused packages (#6032)
* chore(deps): vendor the free-email domain list and drop unused packages

- vendor free-email-domains: its postinstall downloads a CDN CSV and
  overwrites its own domains.json, so the lockfile hash covers the
  tarball but not the installed data
- drop tailwind-merge and autoprefixer from apps/sim, which imports
  neither (emcn and docs declare their own tailwind-merge; postcss.config
  loads only tailwindcss)

* fix(email): split two fused domain entries in the vendored list

Upstream joins mail2moldova.com/mail2molly.com and
smileyface.com/smithemail.net into one entry each, a CSV line-join
artifact that made all four read as work addresses. Splitting them keeps
the list sorted and adds a guard against a naive refresh.
2026-07-28 17:34:57 -07:00
Waleed 805ac3328d chore(deps): upgrade react-email to v6 (#6034)
@react-email/components 0.5.7 to 1.0.12, @react-email/render 2.0.8 to
2.1.0, react-email 4.3.2 to 6.9.0.

Rendered all 21 templates under both versions and diffed: visible text,
clickable links and image sources are identical. The only output changes
are a dropped <link rel=preload> block, which email clients strip with
the rest of <head>, and a margin/padding reset on <body>.
2026-07-28 17:14:56 -07:00
Waleed 69b836455c chore(deps): move to the renamed @daytona/sdk package (#6033)
@daytonaio/sdk was renamed upstream to @daytona/sdk; the old name stops
receiving releases. Import-specifier change only, plus the matching
serverExternalPackages entry.
2026-07-28 17:09:52 -07:00
e8e3d6984c feat(pi): optional multi-provider web search for the coding agent (#5951)
* feat(pi): optional multi-provider web search for the coding agent

Adds a search provider dropdown (Exa, Serper, Parallel, Firecrawl) to the Pi
block, off by default. The selected provider's key comes from the block field or
Workspace Settings → BYOK; a Sim-hosted key is never spent, so a missing key
fails the run with a setup message instead of quietly billing Sim.

Search is available in all three modes. Local Dev and Review Code register a
host-side tool that goes through the existing provider tools, while Create PR has
no host in the loop and gets a generated Pi extension in the sandbox. Both paths
derive their requests from one normalizer and are held together by a parity test,
since the sandbox copy cannot import Sim's code.

Results are normalized to title, URL, snippet, and publication date, capped per
field and per envelope, marked untrusted in the prompt, and limited to 20
searches per run so a tool loop cannot drain the workspace's quota.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): drop the banned JSON round-trip from the search parity test

`check:utils` bans `JSON.parse(JSON.stringify(...))`. The round-trip was
normalizing the host body to its wire form, which buys nothing here: the bodies
are plain JSON and `toEqual` already ignores undefined members.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): upgrade the E2B SDK so long Pi output streams stop failing

Create PR streams the whole Pi run through one Connect server-stream
(`commands.run` -> envd `Process.Start`), held open for the full
`PI_TIMEOUT_MS`. Mid-stream it could die with:

  [internal] protocol error: received unsupported compressed output

That string is `@connectrpc/connect-web`, not Pi — Pi has no Connect
dependency at all. connect's `compressedFlag` is `0b00000001` and gzip's
magic first byte is `0x1f`; `0x1f & 0x01 === 1`, so a raw gzip body fed to
the envelope reader trips this on byte one. It reads as "the server sent a
compressed envelope" but really means "this was never a Connect envelope" —
an HTTP-level gzip that was not transparently decompressed.

e2b 2.30.0 pinned `@connectrpc/connect-web@2.0.0-rc.3` and drove envd
through undici 7 with `allowH2: true`. e2b 2.36.1 moves to stable
connect-web 2.1.2 and loads undici 8.8.0 when Node >= 22.19.0 — exactly our
engine floor — so the failing path gets a different HTTP stack.

The connect-web upgrade alone is not the fix: 2.0.0-rc.3 and 2.1.2 ship a
byte-identical `connect-transport.js` (bar the copyright year), and
connect-web still has no `acceptCompression` option by design. The undici 8
swap is the part that matters.

`@e2b/code-interpreter@2.7.0` only asks for `e2b: ^2.28.0`, so the override
pins the floor we actually need. Verified API-compatible: every method we
call (`Sandbox.create`, `runCode`, `commands.run`, `files.read/write`,
`kill`, `Template`, `defaultBuildLogger`, `waitForTimeout`) has an identical
signature across the two versions, and we never touch `SandboxPaginator`,
the one type that changed.

* fix(pi): correct search normalization edge cases and the budget's stated scope

Follow-ups from review of the web-search work. Each fix lands in both the host
adapter (`normalize.ts`) and the Create PR sandbox copy (`extension-source.ts`),
with the extension test asserting the two produce byte-identical envelopes.

- `usableUrl` was the one provider-controlled field not whitespace-bounded:
  title/snippet/date all go through `collapseWhitespace`, `url` only trimmed.
  Up to 2048 chars of newlines and control characters could ride into the
  envelope. Dropped rather than collapsed — `url` must stay byte-exact to stay
  resolvable, so collapsing would emit a different, still-dead link, and a URL
  carrying raw whitespace is already malformed under RFC 3986.

- `numResults: null` (or `''`, or `[]`) returned 1 result, not the documented
  default of 5: `Number(null)` is a finite 0, so the clamp floor won rather
  than the default. Only a real number or a non-blank numeric string now counts
  as the model having asked for a count.

- Envelope truncation was silent. When results were dropped to fit the 50 KB
  ceiling the model read the short list as the complete answer. It now carries
  a message saying so, and the message is inside what gets measured so the note
  cannot push a truncated envelope back over the ceiling.

- The budget is per *block execution*, not per workflow run: the counter lives
  in the tool spec and both adapters build a fresh one per execution, so a Pi
  block inside a Loop gets the full allowance every iteration. The constant,
  the agent-facing message, and the docs all claimed "per run". Renamed to
  `PI_SEARCH_MAX_CALLS_PER_EXECUTION` and corrected the wording rather than
  tightening the cap, since a shared ceiling would fail late iterations of a
  legitimate fan-out.

- The Search API Key tooltip promised "switching providers clears this field".
  That clear is driven through the collaborative editor setter, so a workflow
  imported, forked, or updated via the API keeps the previous provider's key —
  exactly the case where sending it to a new vendor matters.

Docs also gain a warning that Create PR hands both the model key and the search
key to the agent as environment variables, which Pi copies into every bash
child. That matters most for Settings > BYOK keys: those are workspace-scoped,
only admins can manage them, and the API only ever returns them masked — yet
anyone who can run a Pi block in Create PR mode can read the raw value.

* fix(pi): make the search provider drift guards actually fire

The "you cannot add a provider without mirroring it" story rested on two
mechanisms that did not hold. Verified by adding a fifth provider to
`PI_SEARCH_PROVIDERS` and running the build: it produced only two errors, and
every test still passed.

- `normalizePiSearchRecords` assigns to `let built` inside its switch rather
  than returning, so unlike its two siblings a missing case was not a type
  error — it silently normalized the new provider to zero results. Added an
  explicit `never` check.

- The sandbox copy's `normalizeRecords` used a trailing `else` for Firecrawl,
  so an unmirrored provider was silently normalized with Firecrawl's field
  names; `extractRecords` did the same with its `payload.data` tail. Both now
  test for `firecrawl` explicitly and throw otherwise.

- `Record<PiSearchProvider, ...>` on the `TOOLS` and `payloads` fixtures looked
  like exhaustiveness guards but are inert: `apps/sim/tsconfig.json` excludes
  `**/*.test.ts`, and vitest transpiles without typechecking. Both suites drive
  their providers off `Object.keys(fixture)`, so a missing provider was skipped
  rather than failed. Each suite now asserts its fixture covers the registry.

Re-running the same experiment now yields three compile errors plus two test
failures naming the missing fixtures.

* fix(pi): drop the workspace BYOK fallback for the search key

A fallback exists so a key has somewhere to go when the field is unavailable.
The Search API Key field is unconditionally available: unlike the model key,
whose visibility runs through `shouldRequireApiKeyForModel` and its `isHosted`
branch, `getSearchApiKeyCondition` gates only on whether a provider is
selected. So the fallback never had a configuration to cover.

Removing it also closes an escalation. Workspace BYOK keys are admin-managed
and the API only ever returns them masked, yet `resolvePiSearchKey` would
resolve one for any member who could run the block — and in Create PR that key
is handed to the sandbox as an environment variable, which Pi copies into every
bash child. A member could read a credential the product deliberately never
shows them. Requiring the key on the block keeps the sandbox exposure to a key
its author already holds.

Nothing depends on the fallback: it has never shipped.

- `resolvePiSearchKey` is now synchronous and returns the key, since there is
  no lookup left to await. `byokProviderId` leaves the search registry and
  `PiSearchKeySource` / `PiSearchKeyResolution` are gone — with one source,
  `keySource` carried no information, and the logging rationale for it (a block
  field silently shadowing a stored key) no longer exists.
- The field is now `required`. Safe alongside its condition: the serializer's
  required check returns early for fields that are not visible, so a Pi block
  with search off still validates. Pinned by a test.

Docs and the block's tooltip, placeholder, and best practices updated. The
Create PR key-exposure callout now explains the missing fallback rather than
recommending the block field as a way around it.

* docs(pi): import Callout explicitly, as the sibling block docs do

`fumadocs-ui/mdx`'s `defaultMdxComponents` already provides `Callout`, so the
callout added earlier rendered fine without this — but logs.mdx, credential.mdx,
and response.mdx all import it explicitly and pi.mdx was the outlier. Not a
build fix: the docs Vercel deployment is failing on staging HEAD as well.

* chore(deps): exclude the e2b packages from the release-age gate

CI's `bun install --frozen-lockfile` failed on the E2B upgrade:

  error: No version matching "@e2b/code-interpreter" found for specifier
  "^2.7.0" (blocked by minimum-release-age: 604800 seconds)

This did not reproduce locally because the checkout's bun was 1.2.15, which
predates `minimumReleaseAge` support and ignored the gate outright; CI runs the
pinned 1.3.13 and enforces it.

Excludes only the two packages that are actually too young —
@e2b/code-interpreter 2.7.0 (2026-07-23) and e2b 2.36.1 (2026-07-27). The rest
of the chain already clears the gate: @connectrpc/connect{,-web} 2.1.2 and
@bufbuild/protobuf 2.13.0 and undici 8.8.0 are all older than a week, and `tar`
resolves from the lockfile at 7.5.22 without needing an exception (the original
CI error named only @e2b/code-interpreter, and `bun install --frozen-lockfile
--ignore-scripts` under 1.3.13 now passes locally).

The lockfile is regenerated with bun 1.3.13 rather than 1.2.15, which also
corrects hoisting the older bun had gotten wrong on the merge commit: the root
`lucide-react` hoist moves from 1.23.0 back to 0.511.0 and `@radix-ui/react-slot`
from 1.3.0 to 1.2.2, each with the proper scoped entries. Package resolution
still differs from staging by exactly the e2b chain and nothing else.

Both entries age out on 2026-07-30 and 2026-08-03; drop them then.

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-07-28 16:16:51 -07:00
Vikhyath MondretiandSim Pi Agent 4043341b21 feat(library): Best Gumloop Alternatives in 2026 (#6009)
* feat(library): Best Gumloop Alternatives in 2026

* generate image

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
2026-07-28 11:38:23 -07:00
Waleed 8b199d7199 fix(files): serve rendered documents and stream workspace archives (#5995)
* fix(files): serve rendered documents instead of generator source

Generated docs (docx/pptx/pdf/xlsx) store their generation source as the primary
file; the rendered binary lives in a separate content-addressed artifact store.
resolveServableDocBytes is the chokepoint that swaps one for the other, and the
serve route, single-file download and ~50 tool routes all go through it.

Archive compression and the public v1 download read raw bytes instead, so a
generated document arrived as source text under a .docx name and Word reported
it as corrupt. Both now resolve through the servable reader.

Adds fetchServableWorkspaceFileBuffer beside the raw reader so the record to
UserFile mapping lives in one place, preserving storageContext; the raw reader's
doc comment now says it returns generation source, so the next call site has to
choose deliberately. v1 also has to send the resolved content type rather than
the record's source MIME, and returns a retryable 409 rather than a 500 when an
artifact is still compiling. docNotReadyMessage centralizes the 409 copy, and
isRenderableDocumentName is shared so the read path and its callers agree on
which extensions can expand.

* perf(files): stream workspace archives instead of buffering them

The bulk download route materialized every selected file before writing a byte,
so peak memory tracked the size of the selection. Ordinary files are now
appended as lazy streams: each opens its storage read only when the archiver
reaches it, so one entry is resident at a time rather than the whole archive.

Generated documents still resolve to buffers first. They are the only entries
whose bytes decide anything, and every status this route returns comes from
them — once the first byte is written the status code is committed, so those
decisions have to happen before the archive starts. The per-entry allowance and
byte budget therefore govern documents only.

archiver processes appended entries through a sequential queue; lazystream
defers each storage read until that entry's turn, since handing the archiver an
open stream per entry would hold more connections than the storage client pools.
nodeReadableToWebStream moves out of input-validation.server.ts into a shared
util rather than being written twice: Readable.toWeb throws ERR_INVALID_STATE
when a consumer cancels while the source is still flowing, which is exactly what
a cancelled download does.

Trade: a storage read failing mid-archive truncates the response rather than
returning 500, since the status is already sent. Documents cannot hit this. The
table export route already behaves this way.

* improvement(files): surface archive download errors in place

Bulk and folder downloads navigated to the API route, so any rejection replaced
the Files page with the raw JSON error body. Single-file download already
fetched and saved the blob, so the two paths had diverged.

That was survivable when the only failures were "too many files" and "too
large"; resolving rendered documents adds a 409 for a still-compiling artifact,
which is reachable in normal use. Both archive paths now fetch the zip and show
the server's message as a toast — the route writes that copy for a person, so it
is worth surfacing rather than discarding. Single-file download shows its error
too instead of only logging it.

* improvement(files): simplify the archive route and stop buffering real uploads

Four review passes over the branch converged on the same points.

Routing every office extension through the buffered path held an entire selection
of genuinely uploaded documents in memory — for a check the resolver settles on
the first few magic bytes. Only files stored under a generator-source content
type need resolving; a real .docx serves what is stored and now streams like
anything else, which is what the change was for.

With resolution sequential, the AbortController cancelled nothing (its signal
never reached the storage read), the success-path over-limit branch was
unreachable, and the cancellation guard in the catch could not fire. A plain loop
that returns at the point of failure replaces the outcome record, the sentinel,
the fan-out helper at limit 1, and four post-hoc scans. ZIP_MATERIALIZE_CONCURRENCY
was dead, and the aggregate over-limit message reported a byte count that was by
construction under the limit.

lazystream and its types are gone: Readable.from over an async generator defers
the open the same way and propagates source errors natively, so the PassThrough
relay went with them. The client uses requestRaw with the existing binary
contract instead of a hand-built query string and a re-typed error extractor, and
both archive call sites share one callback. The render headroom moves beside
isRenderableDocumentName, the servable reader stops minting a request id per
file, and the v1 download returns a view rather than a second full copy.

* improvement(files): keep the lazy entry stream in byte mode

Readable.from defaults to object mode; these chunks are bytes headed for an
archive, so the mode is now explicit. Also makes the fire-and-forget bulk
download call consistent with its sibling.

* improvement(files): correct the servable reader's 409 note

Batch callers build the response from docNotReadyMessage rather than through
docNotReadyResponse, so the doc comment now describes the outcome instead of
naming one of the two helpers.

* improvement(files): correct two comments that described the wrong behavior

The resolution loop's comment claimed at most one rendered document is resident.
It is not: every resolved buffer is held until the archive is assembled, bounded
by the request's remaining budget. The loop resolves one at a time, it does not
release one at a time.

RENDERED_DOCUMENT_HEADROOM_BYTES was documented as bounding the expansion beyond
the declared size, but it is passed straight through as maxBytes and is an
absolute ceiling on the rendered document — renamed to MAX_RENDERED_DOCUMENT_BYTES
so the name matches. Download failures also carry a fallback message, so a
transport error surfaces something better than a bare "Failed to fetch".

* fix(files): put streamed files and rendered documents on one byte budget

The budget only counted rendered documents, so streamed entries never reserved
their declared bytes. A mixed selection could clear the up-front declared-size
gate and still ship close to two full limits: ordinary files up to the cap, plus
documents drawing a fresh cap of their own.

Streamed entries ship exactly what they declared, so their share is known before
anything is read and is now reserved up front; documents draw from what is left.

The budget-exhausted rejection also quoted declared sizes, which for a selection
of small sources reads as a tiny total exceeding the limit. That case has no
knowable byte count — the documents have not been rendered — so it now says what
happened without inventing a number.

* fix(files): attach the download anchor and handle a finalize rejection

A detached anchor's click() works in current browsers, but every other download
helper in the app attaches first, and a silent no-op on this path would look
exactly like a download that never started. Not worth the ambiguity for two DOM
operations.

archive.finalize() was fired with void, so a failure after the response had
started became an unhandled rejection on top of the stream error event that
already fails the response. It is caught and logged now.

* fix(files): guard the archive download against concurrent clicks

Navigating to the route meant a second click just re-navigated. Fetching the
archive instead means each click starts another download that holds the whole
zip in tab memory, and the Download button gave no sign anything was happening.

The button now reflects the in-flight download alongside the existing move and
delete states. The ref guard is there as well as the state because two clicks in
the same tick would both pass a state check.

* fix(files): resolve every generated-document source type, not four of five

The archive keyed off a locally enumerated set of source content types that
omitted text/x-pdflibjs — the isolated-vm PDF generator. Those files failed the
check and streamed their generator source under a .pdf name, which is the exact
corruption this change exists to fix.

A canonical five-member set already existed in the file viewer; enumerating a
fourth copy was the mistake, not the missing string. The set moves to file-utils
beside the other file-type predicates, the viewer imports it, and the route asks
isGeneratedDocumentSourceType rather than carrying its own list. A parameterized
test pins all five, so adding a generator without extending the set fails.

* test(files): validate the produced archive with a real zip reader

Every existing test mocks the storage stream and reads the result back with the
same JS library that wrote it, which cannot catch the failure this route exists
to fix: an archive a real zip reader will not open.

This drives 5 MB of non-repeating bytes from an fs stream through archiver, the
lazy entry generator and the Node-to-web bridge, then checks the output with the
operating system's unzip and compares the extracted bytes. Skips rather than
fails where unzip is unavailable.

* fix(files): bound the markdown export's embedded-asset bundling

The embed list is produced by scanning the document body, so its length and the
bytes behind it are whatever the author put there. Every referenced asset was
downloaded at once with no concurrency bound, no per-file cap and no total cap,
so one request could materialize an unbounded number of unbounded files. Its
sibling bulk-download route already had a count cap and a byte cap.

Metadata is now resolved first, which bounds the download from declared sizes
before a byte is read and moves the authorization check off the download path.
Assets are then fetched with bounded concurrency and a per-file cap; a single
unreadable or oversized asset drops out of the bundle rather than failing the
export, which is what the previous allSettled did.

* fix(files): mount rendered documents into the sandbox, not generator source

Mounting a generated document handed the sandbox its generator source under a
.docx name, so a python-docx or openpyxl script failed on a file that looked
fine and the agent debugged code that was correct.

Both branches were wrong, not just the buffered one: with cloud storage the
mount presigns record.key, which is the raw source object, so swapping the
buffered read alone would have fixed only local storage. Source-backed documents
now always take the servable path — they are bounded by the render ceiling, so
routing them through the web process instead of presigning is affordable.

The text/binary decision also keyed off record.type, which for these files is
text/x-docxjs, so a resolved binary would have been decoded as UTF-8. It reads
the resolved content type now.

* chore(deps): regenerate the lockfile against staging's dependency rework

Staging reworked the OTel split, dropped dead deps and declared emcn peers, so
the lockfile is regenerated on top of that rather than carrying a stale merge.
The archive dependency is the only addition; lazystream stays as archiver's
transitive dep now that it is no longer declared directly.

Regenerated incrementally rather than from scratch: a clean resolve fails the
bunfig age gate because minimumReleaseAgeExcludes lists only the darwin and
linux-gnu @next/swc binaries, not the musl and windows variants that a full
re-resolve also pulls.

* fix(files): budget sandbox mounts on rendered bytes, not declared source size

A source-backed document declares the size of its generator, not of the document,
so the per-file and aggregate mount pre-checks were reading a number unrelated to
what was about to be mounted — tiny sources cleared the guard and only afterwards
was the running total updated with the real length.

Those pre-checks now apply only to files that serve what they declare. A document
is capped by the read instead, at the smaller of the per-file limit and the budget
left, and a size rejection is reported in the same terms as the other mount
limits. Same invariant the archive route already had to learn: declared size is
not a bound once bytes can be rendered.

* test(files): cover the two surfaces added without tests

The markdown export route had no test file at all, and the sandbox mount's
source-backed branch was exercised by nothing — both were changed in this PR and
one of them shipped a defect a reviewer caught within minutes.

Export: the embed-count rejection, the declared-bytes rejection landing before
any asset leaves storage, the per-asset download cap, an unreadable asset
dropping out rather than failing the export, and an unauthorized asset never
being read.

Mount: a generated document never presigning its raw key even on cloud storage,
its rendered bytes mounting as base64 rather than being utf-8 decoded off the
source MIME, and the budget rejecting on rendered length.

* fix(files): stop the export caps from rejecting documents that used to work

The caps I picked would have failed exports that previously succeeded: a
screenshot-heavy document can hold more than 100 embeds, and 100 embeds of
unoptimised PNGs can exceed 100 MB. Adding a limit to bound memory is right;
setting it below real usage turns a memory risk into a broken feature.

The byte ceiling now matches the bulk-download route, so the two export surfaces
reject at the same size rather than at two invented ones. The count is only a
guard on the metadata lookups that run before the byte check, so it moves well
above any hand-authored document instead of sitting where a legitimate one
could reach it.
2026-07-27 20:08:19 -07:00
Waleed 6aa3e0e29b fix(logs): match copilot log-grep patterns with RE2 (linear time) (#5987)
* fix(logs): match copilot log-grep patterns literally, never as a regex

`grepSpans` compiled a caller-supplied pattern with a bare `new RegExp`, whose
only guard was a `catch` for syntax errors. Trace text is attacker-influenced —
a workflow can emit arbitrarily long uniform runs into its own block outputs —
and matching runs synchronously on the shared event loop, so an authenticated
caller could choose both the pattern and the input and stall every request on
the instance.

Patterns are now matched as an escaped, case-insensitive literal. Screening was
implemented first and abandoned, because each rule only excludes the shapes
someone thought to enumerate:

- `safe-regex2` (already used by the guardrails validator) documents itself as
  having false negatives. It screens star height only, so it passes `(a|a)*b`,
  measured >61s on V8 at 30 characters.
- Rejecting quantified groups on top of that catches `(a|a)*b`, and every
  attack `safe-regex2` catches, but still passes `a*a*b` — adjacent quantifiers
  over overlapping character sets — measured 213s on JSC and 132s on V8 over a
  10k-character run, well inside what a block output can hold.

An escaped literal is linear on every engine, needs no dependency, and does not
depend on which runtime serves the request. `safe-regex2` stays a dependency for
its existing guardrails/PII callers; it is simply not relied on here.

`query_logs` loses regex matching. Its catalog entry describes `pattern` as
"greps" rather than promising regex, but it is generated from a contract in
another repository and cannot be updated here, so a `patternNotice` is returned
whenever a pattern contains regex syntax — the caller is told the pattern was
taken literally instead of reading zero matches as "not in the trace".

Two bounds are kept as backstops: a cumulative match-time budget charging only
time inside `test`/`exec` (never the blob-store reads the scan awaits between
matches, which would truncate slow-but-legitimate greps under load), and a
total scanned-character cap.

* improvement(logs): only flag genuine regex intent, document the truncated invariant

Review follow-ups on the literal log-grep.

The `patternNotice` fired on any regex metacharacter, which includes `.` — so
ordinary literal searches (`example.com`, `file.pdf`, `v1.2.3`,
`block_1.output`) were told their pattern "was not interpreted as a regex",
inviting the agent to retry a search that had in fact worked. Measured 10 false
notices across 19 realistic literal searches. `REGEX_INTENT` now keys on actual
regex intent — escape classes, character class, group, alternation, a
leading/trailing anchor, or a repetition quantifier — which drops that to 0/19
while still flagging all 10 regex attempts tested.

`truncated` gains a TSDoc block stating its invariant: it reports that trace was
left unread, not that a budget was reached. Every point that skips work goes
through `done()`, which sets it, so a budget exhausted by the final match
correctly reports `false`. Raised by Cursor Bugbot; behavior is right, the
contract was just implicit.

Also drops `snippetAround`'s `maxChars` parameter, which every caller passed
from the state object it already receives.

* fix(logs): match log-grep patterns with RE2 instead of dropping regex support

Restores full regex support on copilot log-grep, which the previous commits in
this PR had removed to close the ReDoS. Deleting the capability was the wrong
trade: RE2JS is a port of RE2 that matches in time linear in the input, so no
pattern can blow up regardless of the text, and callers keep their regexes.

Measured through the real compile path, against a 100k-character adversarial
input — 10x the run that took 213s on JSC / 132s on V8:

  (a+)+$      13.0ms      (\w+\s?)*$   10.7ms
  (a|a)*b      6.7ms      (x+x+)+y      4.0ms
  a*a*b        8.1ms      ^(\d+)*$      0.0ms

Two escape hatches keep this honest:

- A pattern with no regex metacharacter takes the built-in engine. Semantics are
  identical when there is nothing to interpret, and RE2JS costs ~100x more per
  byte (~25ms/MB), so the common plain-text search stays native.
- Lookaround and backreferences are not in RE2. RE2JS rejects them at compile
  time, so those fall back to a literal and return a `patternNotice` naming the
  constructs — a narrow, reported gap rather than a silent behavior change.

The match-time and scanned-character budgets stop being formalities: RE2JS's
throughput is what they now bound, not backtracking.

Also collapses the old test-then-exec double scan — `compilePattern` returns a
single `find` returning a match index, which `recordIfMatch` passes straight to
`snippetAround`, so each field is scanned once instead of twice.

re2js@2.8.6 is MIT, pure JavaScript (no native addon, so nothing changes for the
oven/bun image), server-only, and published 2026-07-05 — clearing the 7-day
bunfig minimumReleaseAge gate without an exclusion.

* fix(security): route every caller-supplied regex through the linear-time engine

Extends the RE2 fix to the three other places that compiled a caller-supplied
pattern and ran it on the shared event loop, behind one shared helper —
`lib/core/security/linear-regex.ts` — rather than four local copies.

- `lib/copilot/vfs/operations.ts` — copilot VFS grep took a caller pattern over
  caller-supplied file content with no screen at all.
- `lib/guardrails/validate_regex.ts` (`validateRegex`) — a guardrail rule's
  pattern matched against LLM output, screened only by `safe-regex2`.
- `lib/chunkers/regex-chunker.ts` — the KB chunker's split pattern. This one
  screened for catastrophic backtracking by running the pattern against six
  probes including `'a'.repeat(10000)` and rejecting anything over 50ms — but
  it measured elapsed time *after* the match returned, so the screen was the
  denial of service it existed to prevent (`a*a*b` on that probe: 213s). The
  probe is deleted rather than repaired.

Deliberately unchanged: `validateRegexPattern`. Those patterns are persisted for
Presidio, a separate service where a slow pattern times out instead of stalling
this loop, and Presidio's Python engine accepts constructs RE2 does not — so
narrowing it to the RE2 subset would reject patterns that work. Its TSDoc now
says plainly that its `safe-regex2` screen is a courtesy, not a ReDoS defense,
and points in-process callers at `compileLinearRegex`. `safe-regex2` therefore
stays a dependency, used only there.

Lookaround is the standard way to split while keeping the delimiter (`(?=#\s)`)
and RE2 cannot represent it, so the chunker keeps the built-in engine for those
patterns — the only remaining path that can backtrack, bounded by the existing
500-character cap. RE2JS `split` was verified identical to `String.split` across
six representative chunking patterns.

Adds 27 tests for the shared helper (every catastrophic pattern asserted linear
over a 50k-character input) plus regression tests at each site, plus the first
tests for `validateRegex`. Also removes the now-duplicated `escapeRegExp` and
literal-finder locals from `log-views.ts`.

* fix(security): drop safe-regex2, keep lookaround splits on the linear engine

Audit follow-ups, plus the Cursor round-4 finding.

Removes `safe-regex2` entirely. Its last caller was `validateRegexPattern`, the
PII custom-pattern boundary, where it was pure cost: it screens star height
alone and passes `(a|a)*b` and `a*a*b`, while rejecting patterns that work —
lookbehind and optional groups could not be saved as custom PII rules at all.
Nor could any screen be sound there: those patterns execute in Presidio's Python
engine, which backtracks on shapes RE2 accepts, so RE2-representability says
nothing about their runtime. Validation is now syntax-only and the dependency is
gone.

Cursor flagged the chunker's lookaround fallback as reintroducing backtracking,
which was correct. The fallback is removed. Verified first that no bounded probe
can replace it: at a probe size small enough to survive an exponential pattern
(24 chars), `(?=a*a*b)` completes in 0.0ms and passes, because polynomial blowup
only shows on long input — small probes miss it and large probes hang, the same
trap as the original guard.

Instead `compileLookaroundSplit` puts the two idioms that actually need
lookaround onto RE2: splitting on `(?=X)` is slicing at every match start of X,
and on `(?<=X)` at every match end, neither of which needs the assertion. Both
`(?=#\s)` and the pre-existing `(?<=</section>)` case keep working, now in
linear time; anything else RE2 cannot represent is rejected with an actionable
message rather than run on the backtracking engine.

Also from the audit:

- `literalRegex` recompiled its `RegExp` on every call to dodge `lastIndex`
  state from the `g` flag. VFS grep calls it per line, so that was a compile per
  line. It now keeps one non-global instance for `test`/`find` and a global one
  for `split`.
- `validateRegex` and VFS grep log a warning when a pattern degrades. A
  guardrail rule that used lookaround now fails closed, which reads as the
  guardrail tripping on every input, and VFS grep's return shape has nowhere to
  report that a pattern was taken literally — both need to be findable in logs.

Measured: RE2 costs 6-13x native on per-line matching (20k-line, 1.66MB file
greps in 7ms), against ~100x on single multi-megabyte strings.

* test(pii): drop the custom-pattern editor's backtracking-screen assertion

CI caught this; my local runs only covered `lib/**` and missed the `.tsx`
counterpart of removing `safe-regex2`.

The editor asserted that `(a+)+$` renders "potentially unsafe". That screen is
gone, so the assertion is inverted: the test now pins that `(a+)+$`, lookbehind
and optional groups all render cleanly, which is the point of the removal —
they are valid Presidio patterns that could not be saved before. Syntactically
invalid patterns still surface "Invalid regex".

* fix(chunkers): support lookaround with an affix, and keep JS whitespace semantics

Three independent reviewers with no shared context audited the RE2 migration.
Two found the same high-severity regression, and one found the divergence that
silently rewrites stored data. Both are fixed here.

**Lookaround with an affix threw.** `compileLookaroundSplit` only accepted a
pattern that was *entirely* one assertion, so `(?<=\.)\s+` — split after the
period — and `(?<=[.!?])\s+(?=[A-Z])` — the textbook sentence splitter — fell
through to the chunker's `throw`. Those are persisted KB configs, and the
compile runs in the constructor, so an existing knowledge base would stop
re-ingesting with a hard error. The TSDoc claiming the bare assertions "cover
the reason a split pattern reaches for lookaround" was simply wrong.

Fixed generally rather than by adding another special case: splitting never
needs the assertion, only the span the delimiter consumes, so `(?<=X)Y(?=Z)`
compiles to `(?:X)(Y)(?:Z)` and group 1's span is exactly what `String.split`
removes. One rule now covers every combination, and the four shapes above match
`String.prototype.split` output exactly.

**`\s` silently lost 14 codepoints.** RE2's `\s` is ASCII-only; ECMAScript's
also covers `\v`, NBSP, U+2028/9, U+202F, U+3000, U+FEFF and the rest of
`\p{Zs}`. A reviewer measured 9 of 35 realistic pattern/document pairs chunking
differently through the real `RegexChunker` — NBSP from PDF/DOCX/HTML
extraction, U+3000 in CJK, U+202F in French text. That rewrites stored chunk
boundaries and embeddings for a document that never changed, with nothing
thrown and nothing logged. `translateToRe2` now rewrites `\s`/`\S` to the
verified-equivalent RE2 class, and `\uXXXX` to `\x{XXXX}` (RE2 rejects the
former outright, turning a non-ASCII delimiter into a hard failure).

Also from the audit:

- `truncated`'s new TSDoc asserted an invariant the code violates — reaching
  `maxMatches` sets it even when nothing was left unread. Reworded to what it
  actually means.
- The chunker's rejection message blamed "backreferences and lookaround" for
  repeat-count and `\uXXXX` rejections too.
- `escapeRegExp` was a byte-identical copy of `executor/constants.ts:472` and
  exported for nobody; now module-private.
- The guardrails wand prompt told the LLM to emit "valid JavaScript regex",
  which reliably produces `(?=.*\d)`-style patterns that now fail every check.
- The PII editor's TSDoc still described the removed backtracking screen.
- VFS grep's TSDoc omitted that invalid patterns now match literally.

Test gaps the audit exposed: the match-time budget's only mechanism was
unguarded (deleting the accumulation left every test green) — now covered by a
mutation-verified test; the escape test survived dropping `.` from the class;
the split-parity test dodged the one real divergence; and two test names
described the built-in engine that no longer runs.

* fix(security): correct three defects in the lookaround split decomposition

A fourth reviewer, given no context beyond "these scanners are new and
unreviewed", differential-tested them against the built-in engine over ~62k
comparisons and found three real defects in code added an hour earlier. All
were reachable through RegexChunker with plausible patterns, and all silently
produced different chunks rather than failing.

1. **Top-level alternation was reshaped into a different pattern.**
   `(?<=\.)\s+|\n\n` means `((?<=\.)\s+)|(\n\n)`, but decomposing it produced
   `(?:\.)(\s+|\n\n)` — demanding the period before *both* branches. It could
   lose boundaries (`"Heading\n\nAlpha beta.\nGamma delta."` split in 2, not 3)
   and invent them. No grouping recovers the original meaning, so patterns whose
   middle alternates at the top level are now declined outright, which also
   removes an asymmetry: `\n\n|(?<=\.)\s` already threw.

2. **A capturing group inside the lookbehind stole group 1.** `(?<=(a))b` cut at
   the assertion instead of the delimiter, and where the group did not
   participate `start(1)` was -1, so `find` and `test` contradicted each other.
   The middle is now captured by name, immune to numbering.

3. **Consuming the assertion text swallowed every other boundary.** The compiled
   form matched `behind + middle + ahead`, so any boundary whose lookahead
   doubles as the next one's lookbehind was skipped: `(?<=\w)\s+(?=[A-Z])` over
   `"A B C D"` split 2 of 3 gaps. Each iteration now resumes at the end of the
   delimiter rather than the end of the match. This was documented as affecting
   only self-overlapping delimiters; that was wrong and far too narrow.

The reviewer confirmed `translateToRe2` and `closingParen` clean across
exhaustive escape-state, character-class, and paren-matching probes, and that
nothing added is superlinear.

One divergence remains and is now documented rather than overstated: when
delimiters themselves overlap (a single-character middle whose matches abut, as
in `(?<=\w).(?=\w)`), a match starting behind the cursor is dropped instead of
emitting the empty segment the built-in engine produces. Splitters that consume
whitespace or punctuation between tokens do not overlap and match exactly.

* test(security): pin engine parity with a differential suite

Every defect in this module has been a silent semantic divergence rather than a
crash, and each was found by comparing engines over a corpus in a throwaway
script. That comparison now lives in the suite: ~700 pattern/document pairs plus
match/index/ignore-case parity, checked against the built-in engine on every
run, with the accepted divergences enumerated and individually pinned.

It earned its place immediately by falsifying my own documentation. The
"self-overlapping delimiter" caveat was stale — fixing the boundary-advance in
the previous commit also made `(?=#{1,6}\s)`, `(?=aa)` and `(?=##)` exact, so
the entry asserting they still diverge failed. One real divergence remains and
is now stated precisely: a lookbehind whose body self-overlaps (`(?<=aa)` over
`aaaaa`). Making that exact means restarting the scan one character past each
match start, which is quadratic on a multi-megabyte document and forfeits the
linear guarantee — so it is a deliberate trade, not an oversight.

Mutation-verified against the three bugs that actually escaped review:
disabling the `\s` translation fails 12 tests, changing the boundary-advance
fails 2, and removing the top-level-alternation guard fails 3. That last one
initially passed, because the corpus had no alternation pattern in it — the gap
is closed and the mutation now fails as it should.
2026-07-27 19:57:30 -07:00
WaleedandWaleed Latif a042f0bd0f chore(deps): fix OTel version split, drop dead deps, declare emcn peers (#5994)
* chore(deps): fix OTel version split, drop dead deps, declare emcn peers

- Pin @opentelemetry/{resources,sdk-metrics,sdk-trace-base,sdk-trace-node}
  to exact 2.7.1 so they match sdk-node's pins instead of floating to 2.8.0.
  The carets meant app code built spans with 2.8.0 and passed them into
  NodeSDK from 2.7.1, which only worked by duck-typing.
- Declare the 13 packages @sim/emcn imports but never declared, as peers
  mirrored into devDeps. @radix-ui/react-dismissable-layer had no
  declaration anywhere in the repo and resolved only transitively.
- Remove ffmpeg-static: its binary downloads via postinstall, but it is not
  in trustedDependencies and Docker installs with --ignore-scripts, so the
  accessSync branch never succeeded and both call sites always fell through
  to system ffmpeg.
- Remove critters + experimental.optimizeCss: Next only loads critters from
  the Pages Router renderer, and apps/sim is App Router only.
- Make simstudio-ts-sdk zero-dependency by dropping node-fetch for native
  fetch; engines >=18.
- Remove unused @vercel/og and postgres from docs, dotenv/inquirer/listr2
  from the CLI, and yaml from the root.
- Move @aws-sdk/client-appconfig from the root to apps/sim, its only consumer.
- Delete the apps/sim overrides block; Bun only honors top-level overrides.
- Bump free-email-domains 1.2.25 -> 1.9.70 (4,779 -> 13,059 domains).
- Validate object/array variables with JSON.parse instead of JSON5, matching
  what the executor actually parses.
- Swap the changelog GitHub icon off lucide to GithubOutlineIcon, matching
  the navbar chip on the same page.
- Unify @types/node on 24.2.1 and lucide-react on ^0.511.0; bump chalk to 5
  and image-size to 2.

* fix(deps): complete the OTel pin, restore SDK error detail, revert email list

Follow-ups from an independent audit of the previous commit.

- Pin @opentelemetry/sdk-node and the three otlp-http exporters to exact
  0.217.0. Pinning only their four dependents was self-reversing: sdk-node
  0.219.0 requires core 2.8.0 exactly, so the next update would have
  silently rebuilt the split this PR removes.
- Declare @opentelemetry/core (2.7.1). It is imported by
  lib/copilot/request/go/propagation.ts but resolved only by hoisting, and
  it is the OTel package with the most version churn in the tree.
- Pin @radix-ui/react-dismissable-layer to exact 1.1.13 in @sim/emcn. All
  five transitive parents pin it exactly; a caret would fork a second copy
  on 1.1.14, which is the duplicate-context bug the declaration prevents.
- Surface error.cause in simstudio-ts-sdk. Native fetch reports network
  failures as a bare "fetch failed" and puts the reason on cause, so every
  DNS/TLS/refused error was reaching callers with no diagnostic content.
- Revert free-email-domains to 1.2.25. Upstream now merges the free-domain
  list with two disposable-email blocklists, so 1.9.70 classifies real
  organization domains as free — UK charities, some companies and
  universities, and the JP/KR ISP domains APAC SMBs use for business mail.
  The demo form blocks submission on that check, so a false positive costs
  the booking entirely. Worth doing deliberately, not inside a deps change.
- Lower packages/cli engines to >=18. chalk 5 and commander 11 both accept
  >=16 and the source uses no Node 20 API, so >=20 only produced EBADENGINE
  for Node 18 users.

---------

Co-authored-by: Waleed Latif <waleed@simstudio.ai>
2026-07-27 19:16:32 -07:00
Waleed 66ac015c4c fix(library): generate every post cover from one template (#5980)
* fix(library): generate every post cover from one template

Three posts shipped an `ogImage` pointing at a file that was never
committed, so the library index rendered broken images and their
`og:image`, JSON-LD, and sitemap entries all 404'd. Several others were
authored without the brand font loaded or with the title clipping off the
bottom edge.

Covers were hand-made per post with no generator, which is why they
drifted. Adds `bun run library:covers`, rendering each cover from the
post's frontmatter title using the reference template already encoded in
the docs OG route, and regenerates all 20 so the grid is uniform.

Line widths come from the font's real advance metrics rather than an
average-glyph-width estimate: the template joins words with non-breaking
spaces to dodge a Satori space-measurement bug, which leaves hyphens as
the only fallback break points, so an under-measured line breaks
mid-compound.

Also drops six orphaned `cover.png` sources left over from the JPEG
compression pass in #5528.

* fix(library): re-render covers every run and add a sync check

Covers are derived artifacts, so skipping outputs that already exist left
an image showing the old title after a post's frontmatter `title` changed.
Every run now re-renders from scratch; rendering is deterministic, so an
unchanged title re-encodes to identical bytes and a full run stays a no-op
in git.

Replaces `--force` (now the default) with `--check`, which renders in
memory and compares against the committed bytes without writing, so CI can
catch both a stale cover and the missing-cover case that caused the
original breakage.

* fix(library): compare decoded pixels in the cover sync check

Byte-equality on the mozjpeg output assumed portable encoder bytes.
libvips/mozjpeg does not guarantee that across OS and CPU, so identical
input can encode differently on a contributor's machine or a Linux CI
runner and fail the check for no real reason — exactly where the check was
meant to run.

Decodes both images to greyscale and compares mean absolute difference
instead, which discards encoder variance while still testing what the
check is about. Measured on this cover set: re-encoding an identical
render with a deliberately different encoder moves it ~0.26, a one-word
title change moves it ~12; the threshold of 2 sits between them with ~8x
margin.

* fix(library): count redrawn pixels in the cover sync check

Averaging the difference diluted a local edit across all 810,000 pixels.
Changing a title's "2026" to "2027" moved the mean by 0.42 — under the
tolerance that absorbed encoder noise — so the check passed a cover still
showing the old year.

Counts pixels that moved more than 48 greyscale levels instead. Measured on
this cover set, that one-character edit redraws 2,559 pixels while three
deliberately different encodes of an identical render (quality 60/70 without
mozjpeg, quality 95 with) redraw none, so the count separates real drift
from encoder variance in both directions.

* fix(library): parse frontmatter with gray-matter and split oversized tokens

Two issues in the cover generator, neither reachable from a current title.

The hand-rolled frontmatter regex could disagree with `gray-matter`, which
is what renders the page and its `og:title`. On a double-quoted escape or a
block scalar the cover would have rendered a title the page never shows,
with `--check` calling it in sync. Uses `gray-matter` directly so there is
one parser.

`wrapTitleLines` only breaks between space-separated words, so a token wider
than the title box on its own stayed on an overflowing line, and the
non-breaking spaces left Satori no recourse but to break it at a hyphen —
the mid-compound break this layout exists to prevent. Oversized tokens now
split here, at hyphens first and per-character only for something like a
URL, and a font size is accepted only if every line measures within the box.

All 20 covers re-render byte-identically, so neither change alters current
output.
2026-07-27 14:19:01 -07:00
Theodore Li 19c3b6f47d feat(setup): setup wizard with browser-based Chat key handoff (#5911)
* feat(setup): setup wizard with browser-based Chat key handoff

Adds `bun run setup` and `bun run doctor` for local installs, and replaces
the wizard's paste-your-Chat-key step with a browser handoff that never puts
the key in a URL.

* improvement(setup): drop the paste-a-key fallback, simplify consent copy

The browser handoff is now the only path — the wizard waits on a spinner
instead of racing a paste prompt. Consent card leads with "Connect your
terminal" and moves the match-the-code disclaimer into the description.

* fix(setup): pin kube context, keep secrets out of argv, validate reused keys

Review findings from #5911:
- helm/kubectl now run against the validated context instead of the ambient one
- helm values are piped on stdin rather than passed as --set arguments
- ENCRYPTION_KEY/API_ENCRYPTION_KEY are checked for the 64-hex format the app
  requires, not just length, so an unusable key is replaced rather than kept
- the managed Redis container's published port is read back instead of assumed

* refactor(copilot): one module for Chat API key operations

list/generate/delete each repeated the same /api/validate-key envelope in
their route. They now share callValidateKey in lib/copilot/server/api-keys.ts,
which also keeps the display masking server-side so the full key can only ever
leave at creation.

* improvement(setup): reuse shared helpers, parallelize probes, drop dead code

- PKCE verifier/state/pairing code now use generateSecureToken, generateRandomHex
  and generateShortId instead of hand-rolled randomBytes; the pairing loop's
  modulo was unbiased only because 256 % 32 == 0
- new sha256Base64Url in @sim/security/hash so both sides of the PKCE exchange
  derive the challenge from one implementation
- isUsableSecret moved beside SECRET_KEYS so setup and doctor apply the same
  rule; doctor previously passed a key setup would replace
- isTruthy narrowed to true/1, matching the app it claims to mirror — it accepted
  yes/on, so a flag could read on in doctor and off in the app
- checkLive runs its five probes concurrently (~17s serial worst case)
- detection overlaps the banner animation instead of queueing behind it
- glyph.fail/glyph.warn at 13 sites that bypassed the constant; removed unused
  prompter exports, a dead ENV_PATHS re-export, and an unused export keyword

* fix(setup): make doctor understand the compose env layout

Compose writes a single root .env (what docker-compose reads via env_file) but
the checks required the three per-app files, so a successful compose install was
followed by doctor printing three failures and exiting 1 — and the whole
coherence catalog was skipped because it keyed off apps/sim/.env existing.

Layout is now derived from what's on disk and every check consults it: file and
schema checks iterate the layout's targets, consistency reports skip when
there's only one file to mirror, and coherence/live read the layout's primary
file. The wizard's existing-config detection counts root for the same reason —
a compose install used to read as unconfigured and re-run from scratch.

* feat(cli-auth): device-authorization poll flow, drop the loopback listener

The CLI no longer binds a local port. It generates a request id + poll secret,
opens /cli/auth, and polls /api/cli/auth/poll over TLS while the user approves
in the browser — so the flow works over SSH and inside containers, where the
browser and terminal don't share a machine.

- approve stores the approval keyed by request id (session-authed, userId from
  the session only); poll verifies the secret before an atomic claim, so an
  observer of the semi-public request id can neither mint nor cancel it
- pairing code stays as the anti-phishing compare; no key ever crosses the
  browser; done page just confirms
- removes the loopback listener, /token exchange, buildCliHandoffUrl, and
  validateCliCallbackUrl (+ its tests) — nothing hands a key to a URL anymore

* fix(setup): reuse an existing managed Postgres container instead of colliding

A running sim-postgres fell through to `docker run --name sim-postgres` and died
on the name conflict; a stopped one failed with "no DATABASE_URL to reach it"
because the generated password only lived in the env files a fresh clone lacks.

Both facts are recoverable from Docker: the ladder now reads the published port
and password back via `docker inspect` and reuses the container (starting it if
stopped). A container that won't answer prompts before recreating, and never
drops the data volume silently.

* improvement(setup): audience-first run-mode hints

Each run mode now names who it's for — compose for self-hosting/evaluating, dev
for contributing to Sim, k8s for rehearsing a production deploy — with the live
detection state (Docker/kube/VM) appended.

* fix(cli-auth): retry a failed mint, port container/port fixes to Redis + k8s

Review findings from #5911:
- poll now reserves the mint with an atomic NX lock instead of deleting the
  approval up front, so a failed mint (e.g. mothership blip) is retried by the
  next poll instead of forcing a fresh browser approval; the lock still prevents
  a double-mint and its TTL frees the slot if the caller dies
- setup reuses/recreates an unhealthy managed sim-redis instead of colliding on
  the name (Redis has no data volume, so it removes and recreates without a prompt)
- k8s failure-path hints carry --context, matching the success-path hints, so a
  changed ambient context can't send diagnostics to the wrong cluster
- compose port-free waits for a killed port to actually release before
  re-checking; SIGKILL is async, so the immediate re-check re-saw the port

* fix(setup): harden mint cleanup, Windows browser, container detection, helm cwd

Review findings from #5911:
- a post-mint completeApproval failure no longer routes into releaseMint — the
  mint lock now outlives the approval (shared TTL), so a cleanup blip can't leave
  a re-mintable window and orphan a key; cleanup is best-effort after the key ships
- compose doctor --fix writes the feature-flag twin to the layout's primary env
  (root .env on a compose install), not always apps/sim/.env
- Windows opens the browser via `cmd /c start "" <url>` — `start` is a shell
  builtin, so spawning it directly ENOENT'd and the handoff never opened
- managed-container detection filters loosely and pins the exact name in code;
  Docker's `name=^x$` anchor matches the internal `/x` form and often missed,
  skipping the reuse branch
- the shared helm/kind run helper pins cwd to the repo root, matching helm test,
  so `helm upgrade --install ./helm/sim` works from any working directory

* feat(chat-keys): standalone manage page, drop from settings nav, refresh README

- Add /account/settings/chat-keys — a linkable page to view, create, and revoke Chat API keys
- Remove Chat keys from the settings sidebar (account + unified nav) and its render branches
- README: replace Docker Compose + Manual Setup with the bun run setup wizard; drop the manual COPILOT_API_KEY step, point to the manage page

* fix(setup): per-key reason in the secret-replacement warning

Cursor: the warn hardcoded '64-character hex key', but only ENCRYPTION_KEY/API_ENCRYPTION_KEY require that — BETTER_AUTH_SECRET/INTERNAL_API_SECRET only need length >= 32. Use the existing secretRequirement(key) helper so each replaced key reports its actual requirement.

* fix(setup): compose doctor schema, cross-platform binary detection, quoted context hints

- Doctor: for the compose (root) env layout, require only the secrets compose has no interpolation default for (BETTER_AUTH_SECRET/ENCRYPTION_KEY/INTERNAL_API_SECRET). DATABASE_URL/BETTER_AUTH_URL/NEXT_PUBLIC_APP_URL come from docker-compose ${VAR:-default}, so a healthy compose install no longer fails doctor.
- Binary detection: use Bun.which instead of which (which is absent on Windows), so kubectl/helm/kind/docker resolve cross-platform.
- k8s diagnostic hints: POSIX-quote the kube-context so a context with whitespace/metacharacters can't break or inject into a copied command.

* fix(setup): quote kube-context in the helm uninstall tear-down hint too

The tear-down hint used --kube-context ${context} raw while the sibling kubectl hints already used shq(); a context with whitespace/metacharacters could break or inject into the copied command. All copyable k8s hints now go through shq(context).

* feat(setup): sim lifecycle CLI — start/stop/status/logs/down/reset

Turn the setup entry into a 'sim' command umbrella so there's one place to run everything, not scattered docker/bun commands. Adds a global bin (bun link) + a bun run sim fallback.

- Detects how you're running (compose file / managed dev containers / helm release) from disk + docker/helm state — no persisted mode. Ambiguous installs prompt.
- start/stop/restart/logs work per mode; down removes containers (volumes kept); reset archives .env + wipes managed data; both destructive verbs confirm first.
- status shows detected mode, container states, and app/realtime health.
- Wizard outro + README now point at the sim commands and the one-time bun link.

* feat(setup): 'bun run sim' is the primary entry; bare invocation prints help

- Lead usage/wizard-outro/README with 'bun run sim <cmd>' (works with zero PATH setup); global bare 'sim' via bun link is an optional upgrade, with the ~/.bun/bin PATH caveat spelled out (Homebrew's bun omits it).
- Bare 'sim' now prints help instead of launching the wizard; the wizard is 'sim setup'. The 'setup' npm script passes the keyword so 'bun run setup' is unchanged.

* fix(setup): quote the auth URL for cmd /c start on Windows

Cursor (High): cmd re-parses the command line and treats & in the query string as a command separator, so cmd /c start opened a URL truncated at the first &, breaking the key flow on win32 (the handoff URL always has request/challenge/pairing). Quote the URL and pass args verbatim so & stays literal.

* fix(setup): verify kube-context is really local; lengthen CLI handoff wait

- k8s: a context named like a local cluster (kind-*, docker-desktop) can actually point at a remote API server. Verify the server host is loopback/docker-internal before defaulting the 'use this context?' confirm to yes; otherwise warn and default to no, so generated secrets can't ship to a remote cluster on a blind Enter.
- cli-auth: bump the device-flow wait from 3 to 15 minutes so first-time users have time to sign up, wait for the email OTP, and approve before the terminal stops polling. The server-side approval record keeps its own short TTL, so a longer client wait only costs cheap rate-limited polls.

* fix(setup): only manage k8s lifecycle on a verified-local context

Greptile: sim down/reset used the ambient kube-context, so switching context after setup could uninstall a same-named sim-dev release from the wrong cluster. Gate k8sInstall on the same locality check the wizard uses (API server is loopback/docker-internal) via a shared isLocalKubeContext helper — the wizard only ever deploys locally, so a remote current-context is never treated as a Sim install.

* fix(setup): doctor skips placeholder secrets when seeding; reset names its target

- checks: the missing-file autofix copied shared keys from apps/sim/.env whenever truthy, including .env.example placeholders — doctor --fix could seed unusable secrets into realtime/db env files. Skip placeholders, matching autofixForMissing.
- lifecycle: reset now names the exact install (k8s context / compose file / dev containers) in its confirm, so a destructive reset can't silently hit the wrong same-named install after a context switch (down already names the context).

* fix(cli-auth): size the poll rate limit to the poll cadence; honor Retry-After

The poll route used the default public-IP bucket (10 burst, 5/min) but the CLI polls every 2s (30/min), so it 429'd within ~20s — worse behind a slow dev cold-compile. Give the endpoint a bucket matched to its cadence (60 burst, 60/min); it's not a brute-force surface (unknown request id returns pending, minting needs the 256-bit verifier). Also make the CLI honor Retry-After and back off on 429 so a shared-NAT per-IP limit degrades gracefully instead of hammering.

* fix(setup): check ports before starting the dev server, not just compose

Local dev auto-start spawned bun run dev:full with no port check, so it silently started a server that couldn't bind when 3000/3002 were already taken (e.g. another worktree's dev server). Extract compose's port-conflict resolver into a shared ensurePortsFree(ports) and run it before the dev start too — kill/recheck/leave, same as compose. Leaving the ports skips the auto-start with guidance instead of failing; compose still treats it as fatal.

* fix(setup): verify the kube cluster is reachable, not just local

A kubeconfig context can outlive its cluster — a kind cluster gets deleted or its Docker container stops (Docker/machine restart), but the context entry remains, pointing at a dead API-server port. The wizard checked the context looked local and handed it to helm, which failed with 'cluster unreachable'.

Add a clusterReachable() liveness probe: only offer the current context when it actually answers; if a local context is dead, fall through to the kind path. There, if kind still knows 'sim' but it's stopped, start its node containers and wait for the API; if it's gone, create fresh. Either way the user gets a working cluster instead of a cryptic helm failure.

* fix(helm): point appVersion at published image tags (v-prefixed, current)

The chart's appVersion was "0.6.73", but CI publishes GHCR tags with a v prefix (its release-commit regex captures v0.7.45). Since sim.image defaults every image tag to Chart.AppVersion, a default helm install requested ghcr.io/simstudioai/{simstudio,realtime,migrations}:0.6.73 — a tag that has never existed — so app and realtime sat in ImagePullBackOff and helm --wait failed with 'progress deadline exceeded'. Any self-hoster installing with default values hit this, not just the setup wizard.

Set appVersion to v0.7.45 (latest release on main; all three images verified present on ghcr) and bump the chart version to 1.1.1. Verified with helm lint, helm template (all images render as v0.7.45), and a live helm upgrade on a kind cluster where the new pods pull successfully while the old 0.6.73 pods remain in ImagePullBackOff.

* Revert "fix(helm): point appVersion at published image tags (v-prefixed, current)"

This reverts commit 28b6047d1d.

* chore(api-validation): rebaseline route count to 977 after staging merge

Staging moved the baseline to 975; this branch's two CLI-auth routes (approve, poll) make 977. The clean merge absorbed the earlier +2 adjustment.

* fix(settings): don't highlight a sibling nav item on nested settings pages

/account/settings/chat-keys is a real page but deliberately not a nav item, so the sidebar's parseSettingsPathSection fell through to defaultSection ('general') and highlighted General — the page read as though it lived inside General.

Resolve the sidebar's active item with a null default so an unmatched nested route highlights nothing, and widen SettingsSidebar's activeSection to string | null. The section feeding the title/description provider keeps its default (pages override title/description anyway), and /account/settings/billing/credit-usage still correctly highlights Billing.

* fix(setup,auth): manage explicitly-confirmed k8s contexts, fail loudly on reset, clear stale post-auth redirect

- lifecycle: detection is now factual — a sim-dev release either exists on the current context or it doesn't. Gating on locality stranded a release the user explicitly confirmed during setup (status/start/stop/down/reset all claimed no k8s install). Locality is recorded instead and surfaced through describeInstall, which every destructive confirm renders, so acting on a non-local cluster is named and defaulted to no rather than silently blocked or silently allowed.
- lifecycle: reset no longer discards helm uninstall's exit status. Env files are archived by that point, so claiming 'Reset complete' while the release still runs is the worst outcome — it now throws with retry/inspect commands.
- auth: signup clears POST_AUTH_REDIRECT_STORAGE_KEY when it has no callbackUrl, and the verification-disabled path consumes it, so a stale CLI/invite destination can't leak into a later flow in the same tab.
2026-07-25 04:24:36 -04:00
Waleed d64739cf4f fix(ci): unblock @next/swc, lockfile-keyed node_modules, per-image runner sizing (#5945)
* fix(ci): key node_modules sticky disk on the lockfile hash

* improvement(ci): per-image Blacksmith runner sizing + cold-build memory preflight

* fix(deps): exclude @next/swc binaries from the release-age gate

* fix(deps): pin @next/swc binaries so frozen installs get a compiler

* docs(ci): explain ARM runner sizing rationale
2026-07-24 16:12:22 -07:00
Waleed 91a9c20acc fix(deps): build isolated-vm on install via root trustedDependencies (#5935)
.npmrc set ignore-scripts=true globally, which overrode Bun's
trustedDependencies allowlist and blocked every lifecycle script —
including the repo's own root scripts. isolated-vm therefore shipped
unbuilt and each contributor had to npm rebuild it by hand.

The .npmrc line landed in the May 2025 Bun migration, seven months before
isolated-vm was introduced. Two later commits added isolated-vm to
trustedDependencies (root, then apps/sim) and both were silently inert.

- delete .npmrc; lifecycle policy now lives solely in root trustedDependencies
- add isolated-vm to root trustedDependencies
- drop the workspace-level trustedDependencies from apps/sim (Bun only reads
  the root list, so it never had any effect)
- drop the .npmrc entry from CODEOWNERS

Naming any package in trustedDependencies replaces Bun's curated
default-trust list, so only isolated-vm and sharp may run install scripts.
Docker is unaffected — all three Dockerfiles pass --ignore-scripts
explicitly and rebuild isolated-vm against the Node ABI by hand.
2026-07-24 12:03:55 -07:00
6dcc65be89 feat(skills): add skill editors (#5705)
* feat(skills): permissions layer

* chore(db): drop skill_member migration 0261 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0262 on latest staging

Same DDL as the dropped 0261 (skill_member table, enums, indexes, skill.workspace_shared)
plus the hand-written write-user backfill, renumbered after staging's 0261.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0263 after staging merge

Staging claimed 0262 (strong_storm); same DDL plus the hand-written
write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(db): drop skill_member migration 0263 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0264 after staging merge

Staging claimed 0263 (workflow_fork_sync_excluded); same DDL plus the
hand-written write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* make editing skills full page

* fix disclaimer

* edit access msg

* fix lint

* chore(db): drop skill_member migration 0264 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0265 after staging merge

Staging claimed 0264 (fat_ikaris); same DDL plus the hand-written
write-user backfill, renumbered on the merged snapshot chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix tests

* simplify system

* fix

* fix lint

* add mship skills docs

* chore(db): drop skill_member migration 0265 for regeneration on latest staging

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): regenerate skill_member migration as 0266 after staging merge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deps): override zod to 4.3.6 to dedupe nested copies breaking type-check

better-auth 1.6.23 and fumadocs-mdx resolve ^4.3.6 to a nested zod 4.4.3,
which makes @sim/auth's inferred betterAuth types non-portable (TS2883) and
split docs onto a second zod instance. Both ranges accept the repo-wide
pinned 4.3.6, so a single hoisted copy satisfies everything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix lint

* feat(skills,tools): fullscreen skill create + shared custom tool editor

Moves the rich-markdown and custom-tool editing surfaces out of modals and
onto full-page surfaces, and collapses the duplicated chrome behind shared
components.

Skills
- Add /skills/new, a full-page create surface mirroring the skill detail page
  (CredentialDetailLayout + DetailSection + unsaved-changes guard). "Add to
  Sim" navigates there instead of opening a modal.
- Import moves to a header action (SkillImportButton) backed by a shared
  readSkillFile helper; the GitHub-URL import and its /api/skills/import route
  are removed.
- Skill name validation is now one shared validateSkillName, replacing three
  copies of the kebab-case rule and its messages.
- The skill editor roster renders through the shared MemberRow instead of
  re-deriving its identity block, with a locked role control and a lock-reason
  tooltip explaining inherited workspace-admin access.

Custom tools
- Extract the canvas modal's schema/code editors into a shared
  custom-tool-editor module (fields, wand generation, schema helpers), cutting
  custom-tool-modal.tsx by ~900 lines.
- Settings > Custom tools gains a full-page detail sub-view (SettingsPanel +
  SettingsSection + saveDiscardActions), deep-linkable via ?custom-tool-id.
  Rows are clickable; delete now lives only in the detail view.
- Replace legacy Button/Input/Badge/Label with the chip family, move chip-field
  chrome into CodeEditor behind an error prop, and delete its dead wand button.

Rich markdown field
- maxHeight is now opt-in: omit it on a page and the editor grows with its
  content so the page owns the only scrollbar. Modals pass explicit caps.
- The field variant drops to font-weight 400 to match adjacent chip fields.

* fix(skills): address review round on create navigation, 409 copy, and editor audit

- Skill create navigated using the first element of the upsert response, but
  that endpoint returns the caller's whole skill list (built-ins prepended) —
  match the new skill by its workspace-unique name instead.
- The suggested-skill 409 toast claimed the skill existed but was not shared
  and told the user to ask a skill admin. Every workspace member can already
  see and use every skill, so a 409 only means the name is taken.
- Adding an editor emitted the skill_shared event and SKILL_MEMBER_ADDED audit
  even when onConflictDoNothing skipped the insert on a concurrent add. Gate
  both on the insert actually returning a row.

* chore: format skills-resolver test import

* fix(skills,tools): audit fixes — autocomplete boundary, resize clipping, error routing

Two real regressions introduced while simplifying the extracted editor:

- The schema-param autocomplete's trigger was rewritten to match a trailing
  identifier, but the completion still split on separators. The two disagreed,
  so typing `data.ci` opened the menu and selecting replaced `data.ci` whole —
  eating the member-access prefix. Both now share one SCHEMA_PARAM_WORD regex.
- The uncapped markdown field measured its height only on value change while
  always setting overflow-hidden, so any width change that re-wrapped lines
  clipped the tail with no scrollbar to reach it. Now re-measures via
  ResizeObserver.

Also from the audit:
- Generation writes bypass the code field's change handler, so an open
  autocomplete stayed over a disabled streaming editor; close it on busy.
- Delete failures rendered in the Schema section's error slot on both custom
  tool surfaces; route them to a toast instead.
- Skill create navigated away while still dirty, stranding the unsaved-changes
  guard's history sentinel so Back landed on an empty create form.
- The Description field on skill create never received its error border.
- Drop a double-applied opacity-50 (the editor already dims when disabled), a
  dead try/catch around a non-throwing call that also shadowed the error prop,
  and a stale reference to a /tools page that does not exist.
- Docs still described the removed GitHub-URL import and the old Add Skill
  dialog; rewrite for the create page and file/paste import.

* feat(tools): read-only tool detail, create lands on the new tool, drop dead wand prompt API

- Viewers without edit rights could not open a custom tool at all, while the
  equivalent skill and custom-block surfaces both offer a read-only view. The
  detail page now takes `readOnly`: editors inert, no Save/Discard/Delete, no
  Generate. Creating still requires edit rights.
- Creating a tool bounced back to the list while creating a skill lands on the
  new skill. Tools now do the same. The upsert returns the workspace's whole
  tool list (newest first) rather than just the new row, so the id is matched
  by title instead of by index — the same trap that produced the skill-create
  navigation bug.
- Remove `openPrompt`/`closePrompt` from useWand. `closePrompt`'s last callers
  went away with the custom-tool-modal extraction and `openPrompt` had none
  before it; nothing reads `isPromptVisible` any more either.

* fix(tools): read-only editors, design-system wrench, skills-matching tool identity

- readOnly never reached the editors: the prop gated actions and Generate but
  the schema and code fields were still typable for viewers without edit rights.
  Wire disabled through both fields into CodeEditor.
- The row icon used lucide's Wrench (strokeWidth 2) where @sim/emcn/icons ships
  one drawn for this system (1.55, tuned viewBox), and it inherited body text
  colour instead of --text-icon. Swap it.
- Give the tool detail page the same identity heading as skill detail: tile,
  name, and description at the top left, instead of only a header title.
- Extract ResourceTile so the skills and tools tiles share one definition
  (SkillTile now composes it), and add an opt-in `iconFilled` to
  SettingsResourceRow so the tools list tile matches the skills gallery. Both
  default to today's behaviour for every existing consumer.

* fix(mentions): use the product's own glyph for every @ mention kind

The `@` menu and the inserted chip mapped kinds to arbitrary lucide icons —
`Sparkles` for a skill, a generic `File` for every file — while the rest of the
product has a settled glyph per resource. Mirror CHAT_CONTEXT_KIND_REGISTRY,
which Chat's `@` menu already renders from:

- skill now uses AgentSkillsIcon, the same glyph SkillTile shows everywhere
- workflow / folder / table / knowledge use the @sim/emcn/icons set the sidebar
  and the chat registry use
- file derives its icon from the filename extension, so a .pdf and a .csv are
  distinguishable, matching the file list and Chat's context chips
- integration keeps the block's brand icon from the registry

Also drop the generic placeholder. `kind` is untrusted — the node schema
defaults it to `''` and a hand-written `sim:` link can carry anything — but an
unrecognized kind now yields no icon instead of a meaningless box, which is what
the chat registry does. The menu already guarded a missing icon; the chip now
does too, so this cannot crash on a malformed link.

* chore(db): drop skill_member migration 0266 for regeneration on latest staging

* feat(db): regenerate skill_member migration as 0267 after staging merge

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Waleed Latif <walif6@gmail.com>
2026-07-23 19:52:00 -07:00
d24bc7eccb feat(agent-stream): thinking and tool streaming (#5671)
* feat(agent-stream): add agent-events thinking/tool streaming for chat and canvas

Ship the agent-events-v1 protocol with provider tool loops, dual-gated chat thinking, DeepSeek/Groq/OpenAI reasoning wiring, and ChatGPT-like thinking chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): clear stuck streaming UI and format db snapshot

Biome was failing CI on migrations/meta/0261_snapshot.json. Also settle
assistant streaming/tool flags when SSE ends without a terminal frame,
without clobbering Stop's finalized content.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): satisfy biome format and import order

Auto-format the sim package for CI lint:check, and repair the Anthropic
streaming tool-loop payload after an unsafe delete-to-undefined rewrite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): keep drained answer on abort and update migration journal test

Treat AbortError from reader.cancel as a cancelled pump result so soft-complete
retains answerText. Point the workspace storage migration journal assertion at
0261_chat_include_thinking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): keep Stop notice when server emits cancel error

Ignore terminal SSE error frames after the user aborts so
"Client cancelled request" cannot overwrite "Response stopped by user".

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(chat): ChatGPT-style thinking shimmer and stick-to-bottom scroll

Add left-to-right shimmer on live thinking label/body, keep scroll working by
shimmering an inner node, and follow the answer only while near the bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): stop pump on client disconnect; soft-complete agents only

Abort the agent stream pump when the projected HTTP body is cancelled so
provider work does not continue after disconnect. Limit AbortError soft-success
to Agent blocks so Function/HTTP cancels still fail in logs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): persist includeThinking across pause snapshots

Paused chat runs with Include thinking enabled were dropping the flag when
serializing the pause snapshot, so resume always rebuilt streams without
thinking/tool SSE frames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): keep drained answer text when stream times out

Persist pump answerText onto the streaming execution before throwing on
timeout, and carry that partial content into the failed block output so
logs match what the client already saw.

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(chat): auto-collapse tools chrome when tool streaming ends

Match thinking UX: open while tools run, collapse when finished, and keep
the panel open only if the user manually reopens it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): settle canvas stream chrome on failure paths

Clear agentStreamActive and settle running tool chips when blocks error,
timeouts cancel runs, or execution ends without stream:done so the output
panel does not stay on live Thinking/Using tools chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(lint): organize imports in terminal console store

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(agent-stream): mark open tools cancelled on HITL pause

Pause can interrupt a tool loop without tool end events; settling those
chips as success incorrectly showed unfinished tools as complete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(db): drop branch-local 0261 migration ahead of staging merge

* chore(db): regenerate include_thinking migration as 0266 post staging merge

* fix(providers): resolve type errors in streaming tool loop call sites

* fix(agent-stream): gate agent events opt-in and correct provider loop behavior

- streamToolCalls and provider thinking requests now require run-level
  agentEvents opt-in (canvas on, chat dual-gated, API off) so existing
  runs keep pre-agent-events behavior exactly
- OpenAI reasoning summaries opt-in + strip-and-retry on unverified-org 400
- streaming loops run tool postProcess again (firecrawl/exa async results)
- bedrock live loop falls back to silent path for responseFormat
- deepseek: reasoning_content pass-back unconditional, 'none' sends disabled
- groq: x_groq.usage fallback, reasoning params gated, qwen none disables
- gemini: functionCall parts echoed verbatim, local ids only for events
- truncated turns (max_tokens/length) no longer execute partial tool calls
- MAX_TOOL_ITERATIONS exit flushes last turn text as final answer
- iterations reports actual model calls; shared loop plumbing extracted

* refactor(agent-stream): consolidate protocol, dedupe client/server plumbing, hygiene

- canonical ChatStreamFrame union + type guards consumed by server emitters
  and the chat client; stream_error restored to legacy log-only handling
- strip thinking/tool args from providerTiming on public final envelopes
- shared tool-chip lifecycle module for chat, canvas, and console store
- shared sink-to-execution-events forwarder replaces the copy-pasted
  adapter in the execute route and HITL manager; LIVE_ONLY event set shared
- stream:thinking payload field renamed data->text; canvas thinking batched
- abort reasons carried as AbortError DOMExceptions so raw fetch consumers
  classify correctly; thinking cap renamed to chars and scope-documented
- kimi wired for agent events like the other compat providers
- deleted dead exports/step-N comments; fixtures match real wire shapes;
  loop tests use explicit mocks instead of importOriginal

* test(agent-stream): cover the dual-gated execution path and typed abort reasons

- chat route tests assert agentEvents reaches executeWorkflow only when
  policy and protocol header agree
- execution-limits tests assert AbortError-typed reasons
- executor metadata type carries agentEvents

* fix(deploy-modal): align include-thinking spacing with the modal's 6.5px rhythm

* docs(agent-stream): autogenerate per-model thinking/tool stream support on the Agent block page

- capabilities.thinking.streamed ('full' | 'summary' | 'none') on models.ts,
  explicit for the Anthropic family where visibility varies per generation;
  getThinkingStreamVisibility exposes the derivation for docs and UI alike
- scripts/sync-agent-stream-docs.ts regenerates the support tables between
  markers in workflows/blocks/agent.mdx from the model registry and
  STREAMING_TOOL_CALL_PROVIDERS; --check fails on drift or missing metadata
- wired agent-stream-docs:check into CI next to the other sync gates

* feat(anthropic): request summarized thinking display for omitted-default Claude models

The newest Claude generations (Fable 5, Sonnet 5, Opus 4.8/4.7) default
thinking.display to omitted — empty thinking blocks, no deltas. On
agent-events runs Sim now opts back in with display: 'summarized', driven
by the registry's streamed metadata; legacy runs keep the exact
pre-agent-events request shape. Registry, generated docs, and the family
capability table updated accordingly.

* docs(skills): cover thinking.streamed and agent-stream docs sync in model skills

* chore(deps): upgrade @anthropic-ai/sdk to 0.114.0 and adopt official types

- adaptive thinking, display, and output_config are now SDK-typed; the only
  remaining custom payload field is output_format (beta-header structured
  outputs, which the SDK models as output_config.format instead)
- anthropic stream events narrow on the SDK's discriminated unions instead
  of anonymous casts; compat deltas type content/tool_calls from the OpenAI
  SDK with vendor reasoning fields as an explicit optional extension
- @sim/auth exposes an explicit VerifyAuth contract so its declarations no
  longer reference better-auth's nested zod instance (TS2883 under fresh
  install layouts); realtime consumer aligned
- docs app zod pinned to the repo's exact 4.3.6 so ai SDK types bind the
  same zod instance (docs type-check was latently broken)
- knowledge embedding tests made hermetic against local .env keys and
  hosted rotation fallback

* refactor(providers): replace legacy as-any stream casts with annotated typed casts

* refactor(providers): finish provider audit — remove dead byte-stream helper, annotate remaining legacy casts

Audit of all 26 providers for the agent-events feature confirmed every
streaming execution declares agent-events-v1 and every adapter emits
AgentStreamEvent objects. Cleanup from the audit: the unconsumed legacy
createOpenAICompatibleStream byte helper is deleted, and the remaining
streamResponse-as-any casts (xai, nvidia, kimi, meta, zai, sakana) are
annotated typed casts matching the groq/deepseek fix.

* feat(streaming): stream answer text live during tool loops via turn_end protocol

The live tool loops buffered all answer text per model turn (classification
of intermediate vs final is only known at turn end), so gated surfaces saw
thinking stream, then dead air with the thinking chrome stuck open, then the
whole answer at once.

Loops now emit text deltas live as `turn: 'pending'` plus a `turn_end`
event per turn. The pump buffers pending text and projects it to the byte
path (answerText/logs/memory/legacy clients) only on a final turn_end, so
all settled semantics are unchanged. Gated surfaces render the pending text
as it streams and reconcile with a reset when a turn resolves to tools:

- public chat: live `chunk` frames from the sink + dual-gated `chunk_reset`;
  byte-path frame emission is suppressed to avoid duplicates (kept for
  response-format transformed streams via clientStreamTransformed)
- canvas: forwarder emits live `stream:chunk` + `stream:chunk_reset`; the
  execute route and HITL resume readers stop re-emitting byte chunks; panel
  chat tracks per-block segments and replaces content on flush
- chat client: per-block text segments, chunk_reset handling, and thinking
  chrome now settles on tool start as well as first answer chunk

* fix(streaming): address validated review findings across provider gating and reset reconciliation

Three-reviewer pass over the branch, findings validated against staging:

- agent-handler forwards agentEvents to executeProviderRequest — the flag was
  computed but dropped in the field-by-field copy, so provider-side thinking
  requests (OpenAI summaries, Gemini includeThoughts, Anthropic summarized
  display) never activated on opted-in runs
- openai: restore summary:'auto' alongside explicit reasoning effort — staging
  always paired them; gating summary purely on agentEvents changed legacy
  payloads
- gemini: Gemini 2 + tools + responseFormat falls back to the silent path;
  the live loop never applied the deferred responseSchema for AUTO tools
- openai-compat loop: malformed tool-argument JSON fails the call instead of
  executing with defaulted {} args (staging parsed inside the execution try)
- openai-compat parser: a vendor id arriving after a synthesized start no
  longer renames the call (start/end ids stayed consistent)
- stream-pump: abort closes the byte projection so a drain blocked on
  backpressure cannot deadlock teardown
- chunk_reset removes the block from the client text order (deployed chat +
  panel chat) so a reset block re-registers at arrival position — fixes
  separator/order corruption when parallel blocks stream around a reset
- resume route echoes the negotiated X-Sim-Stream-Protocol response header
  (parity with the chat route); docs: [DONE] wire shape + final-vs-error
  terminal semantics corrected

* chore(deps): exempt pinned @anthropic-ai/sdk 0.114.0 from the release-age gate

CI's bun install --frozen-lockfile blocks 0.114.0 (published 2026-07-23,
younger than the 7-day supply-chain gate). The pin is exact and was vetted
for the agent-events streaming work; following the existing bunfig pattern,
the exclusion ages out on 2026-07-30 and should be dropped then.

* chore(providers): fix double-cast-allowed annotation placement for the strict boundary audit

The audit only recognizes the annotation on the line directly above the cast;
two annotations had drifted behind intervening code lines (groq stream params,
deepseek loop messages) and the OpenAI reasoning-summary widening cast was
never annotated. No behavior change.

* fix(chat): settle straggler tool chips as error when final reports failure

A failed run can still terminate with a `final` frame carrying success: false;
running chips previously settled green regardless of the outcome.

* fix(canvas): wire agent stream chrome into run-from-block

Run-from-block executions emit the same live stream:thinking/stream:tool
events as full runs but registered none of the handlers, so the terminal
never showed thinking or tool chips on that path. The per-run chrome
(batched thinking writes + tool chip lifecycle + settlement on stream done,
block error, and every terminal execution state) is extracted into a shared
createAgentStreamChrome factory consumed by both paths.

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-07-23 19:39:03 -07:00
Theodore Li 96c67e9123 feat(sandbox): add Daytona as a manual-flip failover for E2B (#5860)
* feat(sandbox): add Daytona as a manual-flip failover for E2B

E2B was a hard single point of failure: lib/execution/e2b.ts had no retry
and no fallback, so a failed Sandbox.create() killed Python function blocks,
JS-with-imports, shell, doc generation and the Pi cloud agent outright.

Extract a SandboxRunner boundary (lib/execution/remote-sandbox) with an E2B
runner and a Daytona runner, selected once per execution by the
sandbox-provider-daytona AppConfig flag. Everything above the provider
boundary — marker parsing, mount materialization, file export, corruption
handling — is unchanged.

Selection resolves before create() and never mid-execution, since user code
has side effects. Each sandbox kind fails closed when its snapshot id is unset.

Notes on the Daytona adapter:
- language binds at create(), not per call: Daytona applies it as a sandbox
  label and silently runs JS through Python if passed to codeRun
- Python routes via CodeInterpreter for its {name,value,traceback} error shape,
  which matches E2B's and keeps formatE2BError's line offsets correct
- timeouts convert ms to seconds
- the streaming path delivers env via the filesystem API, as
  SessionExecuteRequest has no env field and secrets must not reach a command line

Drops the dead E2BExecutionResult.images field (populated, never consumed).

* improvement(sandbox): select the provider by SANDBOX_PROVIDER env var

Replaces the boolean sandbox-provider-daytona feature flag with a
SANDBOX_PROVIDER env var naming the provider ('e2b' default, or 'daytona').
A boolean doesn't scale to a third adapter; a keyed registry does.

- PROVIDERS is a Record<SandboxProviderId, SandboxProvider>, so adding an
  adapter is one entry plus one id-union member — an unhandled provider is a
  compile error, not a runtime surprise
- resolveProvider() reads env synchronously and throws on an unknown value
  (fail fast) instead of an async feature-flag lookup
- drops the sandbox-provider-daytona flag and SANDBOX_PROVIDER_DAYTONA fallback

Verified end-to-end: a Python function block through the running app routes to
Daytona (Creating Daytona sandbox, kind: code) with SANDBOX_PROVIDER=daytona.

* fix(sandbox): gate remote execution by provider availability, not E2B

Addresses the review round on #5860.

- Availability was gated on isE2bEnabled / isE2BDocEnabled, so a Daytona-only
  deployment (E2B_ENABLED unset) had its Python/shell/JS-with-imports and doc
  paths rejected before the provider-neutral sandbox call could run. Replace both
  with provider-aware flags (isRemoteSandboxEnabled / isDocSandboxEnabled) derived
  from the selected SANDBOX_PROVIDER's own credentials + image. E2B behavior is
  unchanged (the E2B branch mirrors the old definitions exactly).
- Make the function-block gate error messages provider-neutral.
- Daytona's streaming runCommand (Pi) returned empty stdout/stderr and delivered
  output only via callbacks, so the Pi cloud flow — which parses markers from
  stdout and formats errors from stderr — saw nothing. Accumulate the streamed
  chunks and return them while still forwarding to the callbacks.

Renames the env-flag exports (and the @sim/testing mock) to match. Adds a
conformance test that the streamed Pi output lands in stdout/stderr.

* fix(sandbox): resolve SANDBOX_PROVIDER case-insensitively

Addresses the round-2 review on #5860. env-flags lowercased SANDBOX_PROVIDER
for the availability gate, but resolveProvider looked up the raw value in a
lowercase-keyed map — so 'Daytona' passed the gate then threw Unknown
SANDBOX_PROVIDER at create. resolveProvider now normalizes casing identically.

* fix(sandbox): use getErrorMessage in build/verify scripts

check:utils flagged the inline `error instanceof Error ? error.message : ...`
pattern in the two new scripts. Use getErrorMessage from @sim/utils/errors,
matching the repo convention the check enforces.

* fix(sandbox): fall back to stdout for Daytona failure text

Daytona merges both streams into stdout and returns an empty stderr, but the
shell-error, base64-export, and URL-mount error builders read only
result.stderr — so Daytona failures surfaced a generic 'Process exited with
code N' / 'base64 failed' / 'curl exited N' instead of the real command output
that the API and agents rely on. Fall back to stdout before the generic message
(provider-agnostic: E2B still populates stderr). Strengthens the shell-error
conformance test to assert the real output surfaces.

* fix(sandbox): enforce timeout on Daytona streaming; drop leftover E2B copy

- Daytona's streaming path (Pi) started the command with runAsync:true and then
  awaited getSessionCommandLogs with no bound, so a hung command never timed out
  the way E2B's commands.run({ timeoutMs }) does. Race the log stream against the
  timeout; on expiry return exit 124 with the accumulated output, and the finally's
  deleteSession terminates the still-running command.
- Two user-facing strings still named E2B after the provider-neutral rename (the
  isolated-vm sandboxPath remediation and the disabled-xlsx message). Made both
  provider-neutral.

Adds a streaming-timeout conformance test.

* fix(sandbox): handle orphaned stream promise + preserve error detail

Two regressions from the previous timeout fix:
- When the timeout won the race, the abandoned getSessionCommandLogs promise
  would reject on deleteSession with no handler (unhandledRejection). Attach a
  .catch that records the error and yields an 'error' outcome, so a late
  rejection is always handled.
- The streaming catch dropped the thrown error, so failures before any chunks
  (env write, executeSessionCommand, missing cmdId) surfaced as empty output.
  Fall back to getErrorMessage(error) when nothing streamed.

Adds conformance tests for the stream-reject and start-throw paths.
2026-07-23 19:19:00 -04:00
Waleed 78fb2c0679 chore(deps): bump next to 16.2.11 to clear security advisories (#5890)
* chore(deps): bump next to 16.2.11 to clear security advisories

Patches SSRF, cache confusion, DoS, and middleware-bypass advisories
(GHSA-89xv-2m56-2m9x et al.) affecting next < 16.2.11 across apps/sim,
apps/docs, and packages/emcn. Excludes next/@next/env from the
minimum-release-age gate until the 7-day window elapses on 2026-07-28.

* chore(deps): drop aged-out typescript entries from release-age excludes

typescript and @typescript/typescript6 passed the 7-day minimum-release-age
gate (aged out 2026-07-15 and 2026-07-13), so their exclusions are no longer
needed. Keeps @typescript/native-preview (permanent nightly builds) and the
Pi packages (age out 2026-07-24).
2026-07-23 10:47:07 -07:00
WaleedandMarcus Chandra 8cce661a37 feat(api): proxyUrl for residential/custom proxy egress on the API block (#5867)
* feat(api): add proxyUrl for residential/custom proxy egress on the API block

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

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

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

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

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

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

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

---------

Co-authored-by: Marcus Chandra <mzxchandra@gmail.com>
2026-07-22 16:24:31 -07:00
Waleed c083be9def improvement(auth): bump better-auth to 1.6.23 and add trusted-proxy client IP resolution (#5857)
* improvement(auth): bump better-auth to 1.6.23 and add trusted-proxy client IP resolution

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

* chore(deploy): expose AUTH_TRUSTED_PROXIES in docker-compose.prod and Helm chart
2026-07-22 14:41:02 -07:00
Waleed a0a437dc99 fix(deps): bump sharp to 0.35.3 and js-yaml to 4.3.0 for security advisories (#5848)
* fix(deps): bump sharp to 0.35.3 and js-yaml to 4.3.0 for security advisories

* chore: fix import order in file-reader
2026-07-22 10:26:10 -07:00
Vikhyath Mondreti 37628f1566 fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant (#5790)
* fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant

* improvement(sockets): state improvements

* address comments
2026-07-21 14:36:11 -07:00
2c4d2091b8 feat(pi): add code review mode (#5577)
* feat(pi): add Cloud Code Review mode and rename Cloud to Cloud PR

Introduce a third Pi mode that reviews an existing GitHub PR in an E2B sandbox and posts a structured review with optional inline comments. Keep the stored cloud id for backward compatibility, extend github_create_pr_review for inline comments, and harden review submission against stale SHAs and invalid comment payloads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(pi): cleanup code

* address comments

* address mor

* address comments

* update

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-07-20 16:25:40 -07:00
Vikhyath Mondreti 345369266d fix(mothership): billing for mcps, hosted key tools (#5740)
* fix(mothership): attribution for direct exec tools

* fix hosted key charges for direct tool exec

* fix llm chat tool
2026-07-17 12:56:03 -07:00
Waleed f6bb8e6d3e feat(storage): native Google Cloud Storage support for self-hosting (#5728)
* feat(storage): native Google Cloud Storage support for self-hosting

Adds GCS as a third object-storage backend with full parity with S3 and
Azure Blob: uploads, streaming downloads, deletes, head, V4 signed URLs
(single + batch), and browser/server multipart uploads via the GCS XML
API. Selection precedence is Azure Blob > S3 > GCS > local disk.

- new provider client at lib/uploads/providers/gcs (cached singleton,
  ADC/Workload Identity or inline GCS_CREDENTIALS_JSON auth)
- per-context GCS_*_BUCKET_NAME config wired through getStorageConfig
- shared getServeStoragePrefix() replaces hardcoded blob/s3 serve paths
- docs (object-storage, environment-variables), .env.example, helm
  values.yaml + values-gcp.yaml storage section

* fix(storage): review round 1 — GCS per-context bucket fallback + ETag quote normalization

- getGcsConfig falls back to the general bucket for every context (GCS bucket
  names are globally unique, so the S3-style sim-execution-files literal default
  would point at an unowned bucket; empty per-context buckets previously made
  uploads and downloads disagree)
- completeGcsMultipartUpload restores quotes on ETags stripped by the shared
  browser upload client before building the completion XML
- docs/.env.example/helm updated for the fallback behavior

* fix(storage): review round 2 — route chat authz and execution-URL detection through getStorageConfig

- getChatStorageConfig delegates to getStorageConfig('chat') (identical for
  S3/Azure, picks up the GCS general-bucket fallback instead of reading the
  raw chat config and rejecting valid chat files)
- parse route resolves the execution bucket via getStorageConfig('execution')
  for all providers, so GCS execution files in the fallback bucket are still
  recognized as our own objects

* fix(storage): validation pass — gcs serve-prefix parity in key parsers + CORS doc fix

- extractStorageKey, extractFilename, and extractEmbeddedFileRef now strip the
  gcs/ serve prefix like s3/ and blob/, so direct-uploaded files on GCS parse,
  delete, download, and embed correctly (previously only the serve route knew
  the prefix)
- file-download storageProvider union includes 'gcs'
- completeGcsMultipartUpload defensively rejects a 200 response carrying an
  XML error document
- docs: CORS example lists concrete x-goog-meta-* header names (GCS matches
  responseHeader entries exactly; wildcards are only supported for origin)
2026-07-17 00:15:14 -07:00
f03b4337fa feat(mothership): mixture of models, search agent, persistent subagents, fork chat, inline questions (mothership v0.8) (#5410)
* feat(scout): add scout agent

* fix(contracts): update contracts to include scout agent

* feat(copilot): search agent (research+scout merge) + read-only table/KB tool handlers

Mirrors mothership dev f90f9b05:
- regenerated tool-catalog/tool-schemas mirrors (search trigger replaces
  research + scout; QueryUserTable / SearchKnowledgeBase entries)
- queryUserTableServerTool / searchKnowledgeBaseServerTool: read-only
  wrappers delegating to the full user_table / knowledge_base handlers with
  hard operation allowlists (and outputPath export rejection on
  query_user_table)
- display maps: 'search' agent label/title/icon added; research + scout
  entries retained so historical transcripts keep rendering
- Search.id replaces Research.id in LONG_RUNNING_TOOL_IDS (it inherits
  research's long crawls)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(copilot): run_code compute-only handler; docs lint fix

Mirrors mothership dev db60da94: run_code is the compute-only variant of
function_execute for the search agent — same sandbox and inputs, no
outputs.files / outputTable, so it cannot create or overwrite workspace
resources. Wrapper handler hard-rejects the write vectors and delegates to
executeFunctionExecute; run_code is deliberately absent from
OUTPUT_PATH_TOOLS and the table output post-processor, so the name gating
blocks writes even for leaked args. Added to LONG_RUNNING_TOOL_IDS,
display title/icon maps, and the regenerated catalog/schema mirrors.

Also removes two ineffective biome suppression comments in the docs
workflow-preview (the rule doesn't fire in the docs app config).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(copilot): failed tool calls must surface their error in terminal data

A failed handler result that carried a defined-but-empty output (the
app-tool executor's 'Tool not found' ships output: {}) won the priority
race in getToolCallTerminalData, so the resume payload's data — the only
thing the model reads — was a bare {} with the error text dropped. The
search agent retried run_code 20+ times blind against a stale server
because every failure rendered as empty instead of 'Tool not found'.

Failed calls now always carry error in their terminal data: merged into
object outputs, wrapped alongside non-object outputs, preserved when the
output already has an error field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(chat): render inline question tags from the agent in chat

* fix(chat): let inert multi-step questions browse all prompts

* improvement(chat): guard question answer formatting against sparse arrays

* chore(copilot): drop user_memory from generated contracts and tool display

Companion to mothership 8ae32e97 (user_memory tool removed — the feature no
longer exists). Regenerates the mothership contract mirrors via
generate-mship-contracts.ts, which also picks up the pending telemetry
contract additions (gen_ai.agent.name labels, llm.client.context_tokens,
llm.client.compactions, llm.request.compaction_trigger, llm.compaction.pause,
gen_ai.usage.context_tokens), and removes the user_memory display title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(chat): answered question card becomes the user turn; two select types only

UI ordering: answering a question card no longer echoes a duplicate user
bubble. The combined answer still goes on the wire as a user message, but the
chat pairs it back to its card (strict 'Prompt — Answer' match, now uniform
for single questions too) and renders the card as the answered recap — the
card IS the user turn, and the next assistant message streams below it. The
pairing is derived from the transcript, so live and reloaded renders are
identical; a dismissed card followed by an unrelated typed message does not
match and renders normally. Messages ending with a question card also drop
the copy/thumbs actions row — the card is an input surface, not a reactable
assistant turn.

Question types are now single_select and multi_select only: text is removed
(the free-text 'Something else' row covers it) and confirm collapses into
single_select with Yes/No options. multi_select rows toggle with a check and
the free-text row's arrow submits the step; answers are comma-joined labels
plus any typed entry. Agent-supplied catch-all options ('Other', 'Something
else', 'None of the above') are stripped at parse — the card always provides
its own free-text row; a question left with no real options is invalid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(chat): question cards are single_select only

Removes multi_select (and its toggle/check UI). The card is one shape: pick
one option or type into the always-present 'Something else' row. Catch-all
stripping and the transcript pairing/recap behavior are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(chat): bring back multi_select question cards

Re-adds multi_select with a reworked interaction: option rows carry real
checkboxes (emcn Checkbox chrome) instead of numbers and arrows, an
option-styled Submit row confirms the step, and the "Something else" row
reads as a plain option until clicked — then it becomes the focused text
box, auto-checks, and can be unchecked without losing the typed text
(blur with nothing typed reverts it). single_select behavior, catch-all
stripping, and the transcript pairing/recap format are unchanged;
multi_select answers are the checked labels comma-joined.

* chore(copilot): regenerate mothership contract mirror (chat blob span attrs)

* chore(copilot): regenerate mothership contract mirror (chat blob metrics)

* feat(secrets): make output of generate api key a secret

* feat(cli): add mkdir, mv, cp to mship tool set

* feat(fork-chat): add fork chat to mothership

* fix(fork-chat): fix messageid handling in fork chat

* feat(credentials): agent-initiated oauth credential reconnect (#5488)

* feat(credentials): agent-initiated oauth credential reconnect

* fix(credentials): address reconnect review findings

* improvement(credentials): log when connect draft name lookups degrade

* fix(conflicts): remove migration

* fix(conflicts): fix conflicts

* fix(fork-chat): add migrations back

* fix(ci): fix lint

* fix(ci): fix bad import

* fix(vfs): fix 500 char limit in vfs for skills and custom tools

* feat(copilot): gate user skills to explicit slash-attach (#5536)

Stop the mothership from adopting a workspace user-skill on its own:

- Remove the load_user_skill tool and its three payload callers (chat
  payload, mothership execute route, inbox executor); delete
  lib/mothership/skills.ts + its test. Skills no longer autoload as the
  agent's own instructions.
- Rename the workspace "## Skills" inventory to "## Agent Block Skills
  — NOT FOR YOU" with a one-line guardrail so a skill's description
  (e.g. "respond like a pirate") is not treated as an instruction.
  Skills reach the model as behavior only via explicit /-attach.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(lots-of-things): lots of things

* feat(subagents): add persistent subagents

* fix(copilot): let edit_workflow set knowledge-base tag filters, and stop it clearing them (#5546)

* fix(copilot): persist KB tag subblocks as JSON strings from edit_workflow

The edit_workflow tool normalizes array-with-id subblocks (via
normalizeArrayWithIds) but only re-stringifies the keys listed in
JSON_STRING_SUBBLOCK_KEYS. `tagFilters` (knowledge-tag-filters) and
`documentTags` (document-tag-entry) were missing, so agent-authored tag
filters were stored as raw JSON arrays while those UI components read
their value with JSON.parse (expecting a string). The result: an agent
edit to a Knowledge block's tag filter persisted correctly but rendered
as an empty filter in the editor (JSON.parse on an array throws -> []).

- Add `tagFilters` and `documentTags` to JSON_STRING_SUBBLOCK_KEYS so
  edit_workflow stores them in the same shape the UI writes.
- Make both components' parsers tolerate an already-parsed array on read,
  self-healing values already persisted in the broken (array) shape.

Search execution was unaffected (parseTagFilters accepts arrays), so the
value was never lost — only the editor render and round-trip were broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(copilot): expose KB tag definitions in VFS meta.json

Surface each knowledge base's defined tags (displayName -> tagSlot) inline in
its meta.json via serializeKBMeta, loaded in one batched query
(loadKbTagDefinitions), so the agent can bind a knowledge-tag filter to a real
tag slot instead of guessing a tag name it cannot otherwise see.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(copilot): stringify KB tag subblocks on the nested-node edit path

The nested-node merge path normalized array-with-id subblocks but never
re-serialized the JSON_STRING_SUBBLOCK_KEYS, so editing a block nested in a
loop/parallel container still persisted tagFilters/documentTags (and
conditions/routes) as raw arrays -- the exact shape the subblock components
cannot JSON.parse.

Route all four write paths through a single normalizeSubblockValue helper so
the normalize and re-stringify steps cannot drift apart again, and extract the
duplicated string-or-array read logic into parseJsonArrayValue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(copilot): tighten subblock serialization helpers

Derive KbTagDefinitionSummary from the canonical TagDefinition instead of
restating its fields, make parseJsonArrayValue generic so callers drop their
`as T[]` casts, and unexport the three builders helpers that no longer have
consumers outside the module now that normalizeSubblockValue fronts them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(copilot): stop stripping tagFilters/documentTags from the agent's workflow view

sanitizeForCopilot dropped `tagFilters` and `documentTags` from the workflow state the
agent reads (workflows/{name}/state.json), while edit_workflow is allowed to write both.
The field was therefore write-only: on a follow-up edit the agent read back an absent
field, concluded no filter was set, and cleared the user's tag filter.

The redaction was introduced for workflow *export* (#1628) and is already enforced there
by sanitizeWorkflowForSharing's key list. The duplicate in the copilot-only
sanitizeSubBlocks was redundant for export and destructive for the agent. Removes it and
pins the contract with a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(copilot): reject malformed KB tag values instead of clearing the filter

`knowledge-tag-filters` and `document-tag-entry` had no arm in the
`edit_workflow` input validator, so they fell through to the pass-through
default. Any non-array value the agent supplied -- a double-encoded JSON
string, an object, an unparseable string -- reached `normalizeSubblockValue`,
where `normalizeArrayWithIds` coerces unparseable input to `[]`. The write
path then persisted `"[]"` over the tag filter the user had configured.

`condition-input` and `router-input` already guard against exactly this and
return an actionable error to the model. Extend that arm to cover the two KB
subblock types. It keys on subblock type, so the unrelated `tagFilters`
short-input on the Algolia block is unaffected. `null`/`undefined` and empty
arrays still clear the field, so intentional clears keep working.

Also wrap `loadKbTagDefinitions` in try/catch. Tag definitions are an optional
meta.json enrichment, but the query ran inside the top-level `Promise.all`, so
a transient failure would reject the entire workspace VFS materialize and
leave the agent unable to read any file. Now it degrades to a meta.json
without tag definitions, matching the sibling materializers.

Adds regression tests for both, plus the first tests for
`parseJsonArrayValue`, the helper that keeps pre-fix raw-array rows readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(copilot): collapse duplicate JSON-array parsing in edit-workflow builders

`normalizeArrayWithIds` and `normalizeConditionRouterIds` each hand-rolled the
same "accept a raw array or the JSON string these subblocks persist" parse.
Extract `parseJsonArray`, which returns null when the value is neither, so each
caller keeps its own distinct fallback: `[]` for the former, the untouched
original value for the latter.

Behavior-preserving. An empty array is truthy, so `[]` and `"[]"` still parse
through rather than hitting either fallback.

`validation.ts` has a third copy, but `builders.ts` already imports from it, so
sharing the helper across the two would introduce an import cycle. Left as is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(copilot): specify tag name and legal operators in KB meta.json

`tagDefinitions` exposed `displayName`, but a `tagFilters` entry must carry the
key `tagName`. An entry written with `displayName` passes validation and
persists, then filters nothing -- a silent failure. Rename the field at the
serializer boundary; the DB column is untouched.

Also emit the operators legal for each tag's `fieldType`, reusing
`getOperatorsForFieldType`. `between` is valid for number and date but not for
text or boolean, and the agent has no way to infer that. An unrecognized
fieldType yields an empty list rather than throwing.

Still unspecified, and deliberately out of scope: a filter entry's value key is
`tagValue` (but `value` on documentTags), and `between` needs `valueTo`. Those
describe the subblock entry shape, not the knowledge base, so meta.json is the
wrong place for them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(copilot): pass a nullish subblock clear through instead of serializing "[]"

`validateValueForSubBlockType` accepts null as an explicit clear, but
`normalizeSubblockValue` then ran it through `normalizeArrayWithIds`, which
coerces any non-array to `[]`, and persisted the string "[]".

No data is lost either way -- "[]" and an absent field both mean "no filters".
But it left the field present when the caller asked for it to be unset, so
`sanitizeForCopilot` showed the agent an empty filter rather than an absent
one, contradicting the absent-means-unset invariant the sanitizer documents.
It also made Algolia's `if (params.tagFilters)` see a set value, since "[]" is
truthy.

An explicitly empty array still serializes to "[]" -- clearing with a value is
distinct from clearing by omission.

Reported by Cursor Bugbot on #5546.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(changes): huge changes

* fix(subagents): lanes

* fix(mship): transcript stuff

* fix(subagents): thinking lanes

* fix(superagent): fix superagent tools and checkpoints

* fix(scope): scope subagent tools

* fix(lint): fix lint

* chore(db): regenerate workspace_files.message_id migration as 0260 on staging base

* fix(superagent): fix superagent integration tools

* improvement(questions): make something else a placeholder

* chore(copilot): regenerate mothership contract mirror after staging rebase

* feat(mship): add external mcps to mship

* fix(ci): fix dev build

* fix(stream): show thinking text

* fix(ci): force redeploy

* fix(mothership): keep chat forks outside workspace storage billing

Preserve the product invariant that Mothership chat files are not charged as workspace file storage after the billing storage merge.

* fix(uploads): restore listWorkspaceFiles throwOnError option dropped in rebase

* fix(subagent-streaming): remove italics

* fix(mothership): treat subagent lanes closed by subagent_end as settled so the between-steps thinking indicator isn't suppressed

* fix(ui): thinking loader and rool names

* fix(ui): add file

* fix(thinking): show thinking during subagents

* fix(chat): drop dead thinking-channel ternary after lanes skip thinking blocks

* fix(thinking): remove thinking text

* improvement(function execute): add timeout to function execute and stop showing text in subagents

* fix(subagents): hide thinking text

* fix(ff): move ff to go

* improvement(superagent): nuke superagent

* feat(main-agent): superagent into main agent

* chore(db): regenerate workspace_files.message_id migration as 0262 on staging base

* fix(credentials): restore reconnect params on shared createConnectDraft

* fix(migrations): rebase with staging

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
Co-authored-by: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-07-16 11:24:46 -07:00
Vikhyath Mondreti 3498b2475a fix(agent): pass through billing attribution to tools (#5698)
* fix(agent): pass through billing attribution to tools

* tests

* fix formatting

* address comments
2026-07-15 16:36:53 -07:00
Waleed 92549844c0 improvement(landing): fix SEO redirects/schema, add FAQ content (#5681)
* fix(docs): remove Ask AI widget

* improvement(landing): add visible FAQ schema, fix SEO redirects and Organization schema

- Move existing plain-markdown FAQ sections in 8 library posts + emcn blog
  into the faq frontmatter array so they emit FAQPage JSON-LD (previously
  visible text with zero structured data)
- Add new, fact-checked FAQ content to the 8 posts with none: openai-vs-n8n-vs-sim,
  v0-5, enterprise, copilot, executor, multiplayer, mothership, series-a
- Add sales ContactPoint to the site-wide Organization schema
- Add 301 redirects for legitimate crawler/dead-link 404s (/$, /&, /Sim,
  /homepage, /logo, /en-US) surfaced by an external SEO audit; left out
  bot-noise paths, already-fixed OG image 404s (#5636), and *.sim.ai
  customer chat subdomains that need manual DB verification, not a
  blanket redirect

* revert(docs): restore Ask AI widget

Ask AI removal is a separate change, out of scope for this PR — restoring
the component, chat panel, api/chat route, and its deps to their
pre-existing state.

* fix(content): correct stale/mislabeled competitor facts surfaced in FAQ audit

- n8n's license is "fair-code" (n8n's own coined term), not "fair-use" —
  a different legal concept entirely; fixed 4 occurrences
- Activepieces' integration count is 750+ per their live catalog, not the
  280+ figure carried over from an older snapshot; fixed 4 occurrences
  across best-zapier-alternatives and n8n-alternatives
2026-07-14 19:53:56 -07:00
Waleed 0d9f8607c1 feat(library): add n8n alternatives, LangGraph alternatives, and open-source AI agent platform listings (#5633)
* feat(library): add n8n alternatives, LangGraph alternatives, and open-source AI agent platform listings

* fix(content): declare actual OG image dimensions instead of hardcoded 1200x630
2026-07-13 11:24:56 -07:00
Waleed fc25cfb3c8 fix(docs): fix Core Web Vitals regressions on docs.sim.ai (#5630)
* fix(docs): fix Core Web Vitals regressions on docs.sim.ai

Empirically measured under real trace-based (devtools) CPU/network
throttling against the live site: mobile Performance 59, LCP 9.2s
(TTFB 745ms + 8.4s element render delay).

- sidebar-components.tsx / [lang]/layout.tsx: the docs sidebar renders
  every page in the doc tree as a link at once. Next's default
  viewport-prefetch fired an RSC payload fetch for every one of them on
  initial load - dozens of concurrent requests competing with the page's
  own content for bandwidth. Wired fumadocs' documented `sidebar.prefetch`
  option through to the custom SidebarItem/SidebarFolder components (which
  were bypassing it entirely, using next/link directly with no prefetch
  prop) via the `useSidebar()` context hook.
- video.tsx: `autoPlay` forces browsers to fetch the full video file
  immediately on mount regardless of `preload`. Gated actual src loading
  behind an IntersectionObserver so a page with several of these doesn't
  pull down every video up front (5MB across 3 requests, in this case).
  Single shared component - fixes every doc page that embeds one.
- proxy.ts: the i18n middleware matcher excluded favicon/robots.txt/etc
  but not `icon.svg`, so every request for it got routed through i18n
  negotiation instead of served as a static file, 404ing in production.
- next.config.ts: enable productionBrowserSourceMaps - safe since this
  repo's source is already fully public, real debuggability benefit,
  zero performance cost.
- shiki 4.0.0 -> 4.3.1 (verified: syntax highlighting still renders
  correctly). Attempted a coordinated fumadocs-core/ui/mdx/openapi
  upgrade to latest; fumadocs-openapi's v11 factory function became
  client-only (breaking change beyond its declared peer deps, requiring
  a component-boundary restructure), so only the safe, verified,
  docs-exclusive bumps (fumadocs-core/ui/mdx, shiki) are included here -
  the openapi major bump needs its own dedicated migration PR.

Verified via a real production build (dummy env, all 3974 pages
including API reference render/build cleanly) and a clean (non-stale)
local server: Performance 59 -> 71 measured under real devtools
throttling, RSC prefetch requests 63 -> 11, video requests/bytes 3/5MB
-> 0. A pre-existing React hydration warning (#418) was found and
confirmed present on live production before any of these changes,
unrelated to this diff - documented, not blocking.

* fix(docs): fall back to eager video loading without IntersectionObserver

The lazy-load gate from the previous commit threw before isInView could
ever become true in environments lacking IntersectionObserver (older
browsers, some embedded webviews), leaving videos permanently
source-less instead of falling back to eager loading.

* chore(docs): drop non-TSDoc inline comments

Repo convention is TSDoc-only, no plain // comments.

* fix(docs): accessibility and SEO defects across the docs app

Audited with parallel subagents against the accessibility and SEO skill
checklists, each fix verified by reading the actual code (not assumed):

Accessibility:
- lightbox.tsx: focus was never captured/restored on close, and Tab
  escaped the modal to the page behind it (no focus trap on the single
  focusable element)
- heading.tsx: the per-heading copy-link icon only appeared on hover,
  invisible to keyboard-only navigation (added peer-focus-visible)
- navbar.tsx: active nav tab had no aria-current
- response-section.tsx: the status-code dropdown had no
  aria-haspopup/aria-expanded/role, and no Escape-to-close
- workflow-preview.tsx: same focus-trap gap as lightbox.tsx on the
  expanded-canvas modal

SEO:
- page.tsx: generateMetadata's hreflang/canonical URLs used a naive
  String.replace to strip the locale prefix, which also matched "/en"
  inside unrelated slugs (platform/enterprise, integrations/enrich,
  platform/self-hosting/environment-variables), corrupting those pages'
  canonical and alternate-language URLs. Replaced with a prefix-only strip.
- structured-data.tsx: the SoftwareApplication JSON-LD block compared
  url === baseUrl (no trailing slash) against the homepage's actual url
  (always has a trailing slash), so the condition was always false and
  this structured data never rendered anywhere, including the homepage.
- structured-data.tsx: "Mothership" in the indexed featureList violated
  the constitution's required language (the agent is "Sim", the surface
  is "Chat") - this ships in JSON-LD search engines parse.

* fix(docs): defer the Ask Sim chat widget's heavy deps until opened

The chat panel (useChat from @ai-sdk/react, Streamdown + its CSS) was
mounted unconditionally in the root layout on every single page, so
its full weight loaded and executed even though the widget starts
closed on every page view.

Traced via the LCP breakdown insight under real devtools CPU/network
throttling: the LCP text element (the intro paragraph) had a ~8s
element render delay despite a ~13ms TTFB, and bootup-time attributed
~4.3s of scripting time to a single chunk containing React/ReactDOM's
own runtime plus this widget's eagerly-bundled dependencies.

Split into a lightweight ask-ai.tsx (just the toggle button + open
state) and ask-ai-panel.tsx (the actual chat UI, useChat, Streamdown),
loaded via next/dynamic(..., { ssr: false }) only when the user opens
the widget. Verified: the panel's chunk now has zero network requests
on initial page load.

Measured (mobile, devtools throttling, /introduction):
- Performance: 69 -> 75
- LCP: 8.0s -> 6.4s
- TBT: 260ms -> 130ms

The remaining ~6.4s LCP delay traces to the same shared chunk, now
identified as core React/ReactDOM hydration cost for this page's
sidebar/TOC/breadcrumb tree rather than an isolated bug - a real,
larger initiative (hydration architecture, not a surgical fix),
documented here rather than rushed.

* fix(docs): preserve Ask Sim chat state across close/reopen

The panel split unmounted AskAIPanel entirely on close, discarding
useChat's message state - reopening always started an empty
conversation, unlike the original single-component layout where
useChat lived in a component that never unmounted.

Fixed by keeping the panel mounted (via a hasOpened flag that never
resets) once first opened, and having the panel itself return null
when closed rather than being conditionally removed from the tree by
its parent - hooks still run every render, so useChat's state persists
across visibility toggles. The dynamic import still only fires on the
first open, so the initial-load win is unchanged.

Verified via a real click-through (open, type, close, reopen): input
persists correctly, and the panel chunk still has zero network
requests on initial page load. Performance unchanged at 75.

* chore(docs): lint fixes (import order, formatting)

* fix(docs): fill the Ask Sim UI gap while the panel chunk loads

handleOpen set open=true synchronously, hiding the trigger button
before the dynamically imported panel had a chance to render anything
(next/dynamic renders null by default with no loading option) - on a
slow connection neither the button nor the panel was visible.

Added a loading fallback in the same fixed position so there's no gap
between the button disappearing and the real panel appearing.
2026-07-13 09:10:33 -07:00
Waleed bbde749dcb fix(rich-markdown-editor): remove the hover block drag handle and + button (#5550)
The hover handle (drag-to-reorder grip + insert button) added surface area and
edge cases for marginal value in a file editor. Block reordering is covered by
the keyboard shortcut (Mod-Shift-Arrow) and block insertion by the slash menu,
so the handle and + are redundant. Removes the component, its styles, the editor
wiring, and the now-unused @tiptap/extension-drag-handle-react dependency. The
highlight text-shift fix from the same feature branch is unaffected.
2026-07-09 16:09:04 -07:00
Waleed ff5353cd83 fix(rich-markdown-editor): highlight text-shift + hover block drag handle (#5543)
* fix(rich-markdown-editor): keep highlight from shifting text

The highlight <mark> added horizontal padding, which pushed the highlighted text (and the
text after it) to the right when the mark was applied. Cancel the padding with an equal
negative margin so the amber tint still bleeds slightly past the text but the text never
moves as a highlight is applied or removed.

* feat(rich-markdown-editor): hover block drag handle (+ / grip)

Adds a left-margin block handle revealed on hover (only when the editor is editable): a + that
inserts a paragraph below the hovered block and opens the slash menu, and a grip that drags to
reorder blocks (via @tiptap/extension-drag-handle) or, on a plain click, selects the block. The
keyboard equivalent of the reorder is Mod-Shift-Arrow (block-mover). The control buttons reset
their own chrome (no default border/background/padding) so they render consistently regardless
of the surrounding reset.
2026-07-09 14:54:13 -07:00
Waleed d078c84ee6 chore(typescript): upgrade to TypeScript 7 (native Go compiler) (#5521)
* chore(typescript): upgrade to TypeScript 7 (native Go compiler)

Bumps typescript to ^7.0.2 across every workspace package. Full
bun run type-check/lint/build/test all pass; apps/sim's type-check
(the one needing an 8GB heap bump) drops from ~55s to ~7s wall time.

Migration fixes required by TS7's stricter defaults:
- baseUrl removed: drop it from 5 tsconfigs (paths already resolved
  relative to tsconfig dir, so behavior is unchanged) and prefix the
  one bare (non-relative) paths entry each in apps/sim and
  apps/realtime with './'
- moduleResolution=node10 removed: switch packages/cli and
  packages/ts-sdk to "bundler", matching the rest of the monorepo
- types now defaults to [] instead of auto-including every @types/*
  package: add "types": ["node"] to the shared base tsconfig (this
  is fundamentally a Node monorepo, so this restores prior behavior
  in one place instead of duplicating it per-package), add explicit
  @types/node deps to packages that now rely on it transitively via
  @sim/db/@sim/logger, and add "declare module '*.css'" to the two
  packages with plain (non-module) CSS side-effect imports that
  TS7's stricter checker now flags
- packages/logger's isomorphic `typeof window` check no longer needs
  DOM lib in every consumer: replaced with `'window' in globalThis`
- packages/testing and apps/realtime's fetch/DOM mocks need DOM lib
  where they're compiled, since they model the browser Fetch API
- the `typescript` npm package no longer exports the classic
  Compiler API from its main entry (moved to unstable/ast subpaths);
  apps/sim's Function-block route used it at runtime to strip
  import statements from user code, so that one call site now uses
  Microsoft's official transition package, @typescript/typescript6
- Next.js 16.2.6's own TypeScript-detection heuristic hardcodes a
  path TS7 no longer ships, and its auto-install fallback assumes
  npm/pnpm; added @typescript/native-preview as a devDependency to
  apps/sim and apps/docs so Next detects a valid native compiler
  instead of trying (and failing) to auto-install one

Not merging yet: TS 7.0.2 published today and is still inside this
repo's bunfig.toml minimumReleaseAge (7-day) supply-chain gate, so
`bun install` will fail for everyone until 2026-07-15. Opening this
now to get it through review; hold the actual merge until then.

* fix(typescript): address Greptile review findings on TS7 upgrade

- packages/logger: 'window' in globalThis treats a shim that leaves
  globalThis.window explicitly undefined as browser-only, silently
  dropping production server logs. Restore the original
  typeof !== 'undefined' semantics via an inline cast instead, so it
  stays correct without requiring DOM lib in every consumer.
- packages/ts-sdk, packages/cli: both are tsc-built, published as
  Node ESM (package.json "type": "module" with an "exports" map).
  "moduleResolution": "bundler" is too permissive for that target -
  it accepts import patterns (e.g. extensionless relative imports)
  that Node's actual ESM resolver rejects at runtime. Switch both to
  "module"/"moduleResolution": "nodenext", the correct pairing for a
  published Node ESM package. Verified real tsc builds (not just
  --noEmit) still succeed for both.

* chore(bunfig): temporarily disable minimumReleaseAge gate for TS7 install

TS 7.0.2 published today, still inside the 7-day gate. Lowering to 0
to unblock this merge; will restore to 604800 in an immediate follow-up
commit right after merging.
2026-07-08 16:41:23 -07:00
Waleed 9d34fbea1b refactor(utils): consolidate duplicated helpers onto @sim/utils (#5509)
* refactor(utils): consolidate duplicated helpers onto @sim/utils

Replaces ~90 hand-rolled reimplementations of error-message extraction,
postgres error-code checks, sleep, Math.random, retry/backoff, object
filtering/omission, noop, string truncation, date/time formatting, email
normalization, and plain-object type guards with the shared @sim/utils
exports. Wires check:utils into CI (test-build.yml) so these patterns
don't regress.

* fix(test): mock @sim/utils/random instead of Math.random in schedule-execute tests

The schedules/execute route previously used Math.random() for jitter delay;
this consolidation PR switched it to randomInt() from @sim/utils/random,
which is backed by crypto.getRandomValues() rather than Math.random(). The
route.test.ts spies on Math.random() no longer had any effect, so jitter
became real random delay instead of the deterministic 0ms the tests expect,
causing intermittent 10s timeouts in CI.

* fix(retry): preserve uncapped Retry-After comparison in tools/index.ts

parseRetryAfter() caps its return value at 30s by default. tools/index.ts
compares the parsed Retry-After against a caller-configured maxDelayMs to
decide whether to skip a retry entirely -- capping before that comparison
silently defeats the skip check whenever maxDelayMs is configured above
30s, since a Retry-After between 30s and maxDelayMs would incorrectly look
"within limits" and get retried instead of skipped (caught by Cursor
Bugbot). Added an optional maxMs param (default unchanged) so tools/index.ts
can request the raw, uncapped value for its own comparison while
backoffWithJitter still clamps the actual sleep duration to maxDelayMs.
Added a regression test covering maxDelayMs > 30s.

* fix(utils): fall back to Intl-resolved abbreviation for unmapped timezones

getTimezoneAbbreviation only covered 9 hardcoded IANA zones and returned
the raw IANA string for everything else, degrading schedule descriptions
for zones like Europe/Berlin or America/Toronto (caught by Greptile). The
deleted local implementation in schedules/utils.ts resolved any valid IANA
timezone generically via Intl.DateTimeFormat's short timeZoneName. Restore
that as a fallback so only genuinely invalid timezone strings return
themselves unchanged.
2026-07-08 11:26:23 -07:00
Waleed cb6f99d518 fix(deps): upgrade better-auth 1.6.11 -> 1.6.13 (GHSA fix, Dependabot #166/#167) (#5508)
Patches a high-severity stored XSS in the oidc-provider and mcp plugins
via javascript:/data: redirect_uri schemes. Also bumps @better-auth/sso
and @better-auth/stripe to matching 1.6.13 peers. Patch-only release
(1.6.11 -> 1.6.12 -> 1.6.13), no breaking changes in either changelog.
2026-07-08 10:29:48 -07:00
Waleed b686111082 feat(textract): migrate to AWS SDK, add AnalyzeExpense and AnalyzeID (#5456)
* feat(textract): migrate to AWS SDK, add AnalyzeExpense and AnalyzeID

- Replace hand-rolled AWS SigV4 signing with @aws-sdk/client-textract, matching sibling AWS integrations (secrets_manager, s3, sts)
- Add Analyze Expense operation (invoice/receipt structured extraction) via AnalyzeExpense/StartExpenseAnalysis+GetExpenseAnalysis
- Add Analyze Identity Document operation (AnalyzeID) with optional back-of-ID page
- Add an operation selector to the textract_v2 block; defaults to the existing Analyze Document behavior for backward compatibility
- Add tests for tool body/response mapping and route-level AWS response normalization

* fix(textract): forward URL documents and fix ambiguous error status

- textract_analyze_expense/textract_analyze_id now fall back to filePath/filePathBack when the document input is a URL string rather than an uploaded file object, so advanced "File reference" URL inputs actually reach the API (Cursor Bugbot)
- mapTextractSdkError defaults to 500 (not 400) when the AWS SDK error has no HTTP status, since that implies a server-side/network failure rather than a bad request (Greptile)

* fix(textract): stop stale processingMode from hiding ID document fields

- Front-document fields (fileUpload/fileReference) are shared across all 3 operations; gate them with a values-aware condition so switching to Analyze Identity Document keeps them visible even if a stale processingMode='async' is left over from a previous operation
- S3 URI field now also requires operation !== 'analyze_id', since that operation never supports S3 input

* fix(textract): preserve first-page metadata across async pagination

pollTextractJob's merge callbacks spread only the latest page, dropping
any field (DocumentMetadata, model version) the first page had but a
follow-up NextToken page omits. Merge accumulated first so later pages
only override fields they actually return.

* fix(textract): pass through the real upstream status for filePath fetch failures

fetchDocumentBytes hardcoded 400 for any non-OK response from a document
URL, masking transient 5xx failures from the document host as client
errors and blocking tool-execution retries. Use the actual response
status instead.

* chore(textract): drop redundant inline comments
2026-07-06 19:42:36 -07:00
Waleed 4086750ddc feat(comparison): add Sim vs Competitor comparison pages (#5383)
* feat(comparison): add Sim vs Competitor comparison pages

- New /comparison hub + /comparison/[provider] detail pages for Sim vs
  16 competitors (n8n, Zapier, Make, Gumloop, Workato, Retool,
  Pipedream, OpenAI AgentKit, Tines, StackAI, Power Automate, Vellum,
  Claude Cowork, Langflow, Flowise), each with ~60 sourced, dated facts
  across platform, AI capabilities, integrations, pricing, security,
  observability, and support
- Data layer at lib/compare/data (types, per-competitor profiles) is
  UI-free and independently auditable
- BreadcrumbList, ItemList, and FAQPage JSON-LD per page; sitemap picks
  up all pages automatically via ALL_COMPETITORS
- Two new fact categories (parallel execution, Agent2Agent protocol)
  researched and sourced against every competitor's own docs

* improvement(comparison): neutralize tone across competitor and Sim fact copy

- Remove promotional/superlative adjectives applied to competitors
  (Zapier "one of the largest catalogs", Workato "notably wide",
  Retool "seamless"/"trusted solution", OpenAI "cutting-edge",
  Flowise "deep"/"mature"/"most widely adopted", Make "robust")
- Reword the few places Sim's own accurate limitations read as
  unflattering rather than neutral fact (dynamic tool use, model
  fallback, durability, async execution, support channels, tracing)
  without changing the underlying facts
- Restore the "Yes:"/"No:" value prefix on two Sim facts where it was
  accidentally dropped during rewording, since FactValue depends on
  that prefix to render the check/X status icon

* fix(comparison): address review findings from Greptile and Cursor Bugbot

- Fix parseFactValue regex to require a word boundary after Yes/No, so
  values like "Not documented"/"Not publicly documented" no longer get
  misread as a boolean "No" and render as neutral text again (Cursor)
- Reword the self-hosting FAQ question to drop the false presupposition
  that the competitor doesn't self-host, which was wrong for n8n,
  Langflow, and Flowise (Greptile)
- Rename isLastRow -> isNotLastRow in comparison-table.tsx to match
  what the variable actually computes (Greptile)
- Move critters to devDependencies; it's a next build -time-only CSS
  inliner, not needed at runtime (Greptile)
- Sitemap lastModified for comparison pages now matches the max(Sim,
  competitor) verified-date logic each page's own JSON-LD dateModified
  already uses, so the two never disagree (Cursor)

* fix(comparison): fix doubled Yes prefix and missing period in hub FAQ

- Two hub FAQ answers prepended "Yes." to fact values that already
  start with "Yes:", producing "Yes. Yes: ..." in visible copy and
  FAQPage JSON-LD. Export and reuse ensurePeriod instead of a hardcoded
  prefix.
- The integrations-count FAQ answer ran two sentences together with no
  period before "Combined with...". Fixed with the same ensurePeriod
  helper.

* improvement(comparison): remove redundant Key differences at a glance section

The 5 facts it previewed (self-hosting, environment promotion,
human-in-the-loop, pricing model, data residency) are the exact same
rows shown again immediately below in the full comparison table, so
the section read as pure repetition for a human scrolling past it.

* fix(comparison): strip Yes/No prefix before lowercasing in summarizeFact

summarizeFact fed the raw fact value through lowercaseFirst even when
it started with "Yes: "/"No: ", producing broken mid-sentence FAQ text
like "n8n: yes: many providers via dedicated Chat Model nodes." Strip
the boolean prefix first, matching Greptile's suggested fix.
2026-07-02 21:25:33 -07:00
Waleed f33d325a88 fix(deps): bump echarts to 6.1.0 to patch XSS vulnerability (#5374)
Fixes GHSA-fgmj-fm8m-jvvx / CVE-2026-45249 — Lines series tooltip
rendering could execute raw HTML from series.data[i].name when no
custom tooltip.formatter is set.
2026-07-02 10:46:50 -07:00
ad19f7fc40 improvement(landing): refine hero and mothership visuals (#5181)
* stash

* feat(landing): mothership feature stages + pre-footer CTA

Tell-then-show landing: the Mothership section defines the five capabilities
(Mothership · Pod · Formation · Dispatch · Return); the Features section now
shows each as a real Sim UI callout floating over a static, edge-faded platform
backdrop (Linear's "callout over a faded platform" pattern).

- FeatureStage template: copy + masked static LandingPreview + elevated callout
- LandingPreview: static autoplay=false snapshots with per-stage view/workflowId
- Callouts: Mothership chat, model picker, parallel-agents Formation graph,
  deploy targets, logs table
- Pre-footer CTA set over the Mothership render; removed the old capabilities grid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): reusable platform-page + solutions-page layouts and routes

Add config-driven, padding-safe layouts consumed by route pages:
- platform-page: hero (shared CTA) + centered logos + N card rows (3|4),
  JSON-LD, single <h1>, server-only; Workflows route as first consumer.
- solutions-page: structural mirror (kept separate to diverge later);
  IT, Engineering, Finance, Compliance, HR routes under /solutions.
- Hoist shared LandingShell/HeroCta/Logos to components/ (top-level =
  shared); refactor hero to consume them.
- Restructure all of (landing) to the workspace folder-per-component
  convention (each component in its own folder + index.ts barrel).

* refactor(landing): convert hero-visual CSS-module keyframes to Tailwind

Move the hero-visual + stage-home keyframe animations out of CSS modules
into tailwind.config (matching the existing dash-animation pattern) and
delete both module.css files. Components now use animate-hero-* utilities
+ arbitrary properties for the per-element delays, SVG stroke draw, and
gradient shimmer; reduced-motion preserved via motion-reduce: variants.
Upgrade the shimmer's hardcoded #b4b4b4 to the --text-subtle token.

brand-tokens.module.css is intentionally kept: it reassigns --surface-*/
--text-* token VALUES via a doubled-class selector for specificity over
.light, which Tailwind utilities cannot express.

* refactor(landing): move brand palette from CSS module into LandingShell

Replace brand-tokens.module.css with a BRAND_TOKENS constant of Tailwind
arbitrary-property utilities applied on the LandingShell wrapper, so the
brand hex lives in the component, not a stylesheet. They emit in the
utilities layer and override .light (@layer base) by cascade order —
verified the brand --text-primary (#121212) wins over .light (#1a1a1a).
No more .module.css files remain in the landing.

* chore(landing): remove Testimonials from the home page for now

Drop <Testimonials /> from the landing composition (component kept for
re-adding later).

* feat(landing): hero send→loader→workflow animation + landing WIP

Hero visual: clicking send zooms into the button, morphs the disc into the
gooey thinking loader (held, then cycling), slides it straight across to a
phrase indicator with the camera following (no zoom-out), then zooms back out
as the reply types and the chat morphs into the GitHub→Agent→Jira workflow.
The chat card holds a fixed size through the zoomed scene and the greeting
reserves its space, so nothing drifts; the user bubble reveals only on
zoom-out. Loader ink tweens dark→gradient via the thinking-loader
stop-color/flood-color transition.

Also folds in in-progress landing work: knowledge + integrations feature
callouts, CTA chat, mothership + line-glyph, wordmark tweak; removes the
ethos and testimonials sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): responsive pass for iPad + mobile

Make the landing page fully responsive while keeping the desktop layout
byte-identical (desktop classes stay the unprefixed baseline; smaller
screens layer max-* overrides on top).

- Navbar: hide desktop clusters below lg, add MobileNav hamburger sheet
  (scroll-lock, Escape/tap close, reduced-motion aware)
- Hero: collapse the absolute split (visual + logos) to a stacked column
  below xl so iPad-landscape avoids the headline/visual collision
- Mothership: 4-col grid steps to 2 (tablet) then 1 (phone)
- Features: drop the floating callout below md, show the un-masked
  backdrop preview full-width
- CTA + Footer: scale type/padding; footer 7-col steps to 3 then 2
- Document the breakpoint strategy in the landing CLAUDE.md

Also includes the in-progress mothership goo/iso brand marks and the
marks-lab preview route the section depends on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): align hero visual panel to text + logos extent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(landing): delay hero user bubble until card finishes expanding

The grey user bubble's fade-in raced the card's upward grow on send.
Hold the bubble's reveal until after the parent-driven grow settles so
the card expands fully before the bubble appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(landing): isolate new landing — remove dead old-folder code + --landing-* coupling

- Delete dead (landing) auth-modal (a duplicate of (home)'s, still on the old
  --landing-* / dark tokens) — its removal drops the new landing's last styling
  tie to the old landing.
- Delete 5 merge-orphaned, zero-consumer callouts (deploy-callout,
  mothership-chat-callout, mothership-chat-preview, workflow-graph-preview,
  model-picker-preview).
- Relocate the one live preview (logs-table-preview) into its consumer
  features/components/ + add a barrel; dissolve the owner-less feature-callouts/ shell.
- Fix stale --landing-bg-surface reference in landing-preview-mount.

(landing) now has zero (home) imports and zero --landing-* token usage.

* refactor(landing): token-map hex, fix a11y/SEO, align structure

Styling (within (landing)):
- Replace ~90 hardcoded hex colors with the in-scope brand tokens they already
  equal (--surface-*/--text-*/--border*); divider edges -> --border, field/card
  edges -> --border-1. Delete the redundant C color-palette mirrors in the
  landing-preview home/sidebar and route them through tokens.
- Convert static inline SVG styles (display:block/outline:none) to Tailwind.
- 6 un-tokenizable hexes remain (dark send-button fills, status-green dot) —
  no brand token exists; left as-is.

a11y / SEO:
- Decorative mothership goo/iso marks: role='img'+aria-label -> aria-hidden.
- Preview chrome titles <h1> -> <span> (kills duplicate client-only H1s).
- sitemap.ts: add /workflows and the five /solutions/* routes.

Structure:
- Folder the bare logo-mark/mobile-nav leaves + barrels; complete the navbar
  components barrel and consolidate navbar.tsx to a single barrel import.

* style(landing): restore taller hero panel with border-shadow chip chrome

- Revert the right visual panel to the previous full-height framing
  (top-8 bottom-8) — hero text (pt-[112px]) and the logos panel are untouched,
  so their positions and spacing are unchanged.
- Apply the canonical border-shadow chip surface: --surface-2 fill + the shared
  chipBorderShadowRing (1px hairline ring + soft drop shadow) from emcn, the
  documented chrome for a landing media panel.

* feat(landing): swap Volvo for thinkproject and reposition hero logos

- Replace Volvo with the thinkproject wordmark (official SVG, tagline/descriptor
  cropped out, all paths unified to --text-primary #1a1a1a; aspect 6.01).
- Reorder the shared 6-logo set so the 3x2 hero grid reads: Rivian|VW (top-left),
  eXp Realty (top-center), Russell (top-right); Artie (bottom-left),
  thinkproject (bottom-center), Mobile Health (bottom-right).
- Enlarge Rivian|VW a touch (height 15 -> 17, same aspect).
- eXp Realty, Artie, Russell, Mobile Health, Rivian|VW all retained.

* style(landing): size hero description with the type scale (text-lg)

Replace the arbitrary text-[20px]/text-[16px] on the hero description with named
scale tokens — text-lg (18px) desktop, text-md (16px) on phones — a touch smaller
and the canonical lead size (1.2x the platform's 15px base).

* style(landing): hero headline "for AI automations" with break after "agent"

Replace "solving automations" with the higher-intent "AI automations" and move
the line break after "agent" so "for AI automations." sits on the second line.

* style(landing): unify CTA radius and box hero logos in cards

- HeroCta email bar: rounded-[13px] -> rounded-lg, so the bar, the inset
  Book-a-demo chip, the Sign-up chip, and the navbar chips all share one radius.
- Hero logos: box each wordmark in a bordered --surface-1 card (platform card
  chrome: rounded-lg + --border-1, 100px tall) on a responsive 3-up grid
  (2-up on phones) at a consistent gap-5 rhythm. Wide marks scale to fit
  (max-w-full h-auto). The platform/solutions 'row' layout stays bare wordmarks.

* style(landing): concentric CTA bar radius + tighter logo cards

- HeroCta email bar back to rounded-[13px] (= inner chip 8px + ~5px inset) so the
  Book-a-demo chip's right corners nest concentrically inside the bar.
- Logo cards: smaller and tighter — h-20 (80px), px-4, gap-3 (12px, the product
  UI card-grid rhythm).

* style(landing): restore 100px logo cards, scale icons down 15%

The 80px cards read too wide-for-their-height. Restore h-[100px] (keeping the
tighter gap-3/px-4) and instead shrink the wordmarks to 0.85x their optical size
in the grid via GRID_ICON_SCALE — row layout unchanged.

* style(landing): match sign-up radius to email bar + shrink logo icons

- Sign-up chip overridden to the email bar's rounded-[13px], so the two hero CTAs
  share one corner radius.
- Logo icons: GRID_ICON_SCALE 0.85 -> 0.65 and card padding px-4 -> px-2; card
  dimensions (h-[100px], gap-3) unchanged.

* style(landing): shrink hero logo cards

Cards read massive — too tall (100px) and stretched to fill the panel. Drop to
h-16 (64px), cap width at w-[150px], and make the grid w-fit so it hugs the cards
instead of stretching. gap-3 and the 0.65 icon scale unchanged.

* style(landing): upscale hero logo cards ~25%

Cards read too small. Bump all dimensions: h-16->h-20 (80px), w-[150px]->w-[180px],
px-2->px-3, and icon scale 0.65->0.8. Grid stays content-hugging at gap-3.

* style(landing): taller logo cards, larger icons, reorder top row

- Card height h-20 -> h-[88px] (width w-[180px] unchanged), icon scale 0.8 -> 0.85.
- Top row reordered: eXp (left), Russell (center), Rivian|VW (right).

* style(landing): more card height, swap top-row Rivian/eXp back

- Card height h-[88px] -> h-24 (96px); width unchanged.
- Top row: Rivian|VW (left), Russell (center), eXp (right).

* feat(landing): add "Trusted by technical teams at" label above hero logos

Top-left, gap-3 above the logo grid (matching the grid rhythm); text-sm (navbar
text size) in --text-muted (the label token).

* style(landing): recolor logos to --text-body, match label gap to hero rhythm

- Recolor all six customer logo SVGs to #3b3b3b (--text-body light value), so they
  match the Sim navbar wordmark's color. Landing is light-only, so the hardcoded
  value always equals var(--text-body).
- Trusted-by label gap gap-3 -> gap-[22px] (the hero's description->CTA spacing).

* style(landing): scale hero CTA down a hair, drop radius to the nav chip's

Sign-up read too round. Take the bar + Sign-up to h-[40px] / rounded-lg (8px, the
navbar chip radius), and keep the inset Book-a-demo concentric: h-[2em] + rounded
(4px) with a 4px inset (8 = 4 + 4).

* style(landing): round Book-a-demo to rounded-md to match the bar curve

rounded (4px) read too square next to the bar's rounded-lg (8px). Bump to
rounded-md (6px) — echoes the bar's curvature, still inside the 4px inset.

* style(landing): match Book-a-demo proportions to the navbar chip

Restore h-[2.143em] (the chip's 30/14 height ratio); with px-[0.571em] (its 8/14
padding ratio) and the 16px label, Book-a-demo now shares the navbar chip's exact
height/padding/text proportions.

* style(landing): equal inset around Book-a-demo (h-[30px])

Button was h-[2.143em] (34.3px) -> only ~1.9px top/bottom vs 4px right inside the
bar's 38px inner box (40px minus the 1px border). Drop to h-[30px] (the nav chip
height) so it centers to an equal 4px inset on top, bottom, and right.

* style(landing): enlarge Book-a-demo to h-[32px], tighten inset to 3px

h-[30px] read too small/airy in the bar. Bump to h-[32px] and pr-[4px] -> pr-[3px]
so the inset is an equal, snugger 3px on top, bottom, and right.

* style(landing): lift hero logos off the bottom again (pb-20)

Restore the 80px bottom padding so the logos rest 112px above the section bottom
(mirroring the hero text's 112px top) instead of sitting flush with the visual
panel's bottom. max-xl:pb-0 keeps the stacked layout tight.

* improvement(landing): refine hero and mothership visuals

* fix(landing): cap hero fold height so it doesn't stretch on huge monitors

The section was min-h-[calc(100vh-62px)], so on very tall displays both absolute
panels (top-8 bottom-8) stretched — the visual panel grew gigantic and the
bottom-anchored logos sank to the very bottom. Cap the fold at 960px via
h-[min(calc(100vh-62px),960px)] (min-height can't be capped by max-height): the
whole hero stops growing, panels/logos stay proportioned like a large laptop, and
the next section just starts below. Laptops (<=16in) are unaffected; max-xl:h-auto
keeps the stacked layout below xl.

* refactor(landing): session cleanup — DRY CTA label, drop dead grayscale

Final tidy after this session's hero/CTA/logo iteration:
- hero-cta: extract the duplicated 16px label knob (px-[0.571em] + text-[16px] +
  font-size:inherit) into a single CTA_LABEL constant, matching the 'single knob'
  the TSDoc already describes — used by both Book-a-demo and Sign-up.
- logos: remove the grayscale filter (now a no-op — all wordmarks were recolored
  to a single #3b3b3b), inline the single-use LOGO_GAP_X, and flatten the nested
  cn() into plain layout ternaries (dropping the now-unused cn import).

* improvement(landing): animate mothership illustrations

* style(landing): solid-ink branding + hero cursor/loader polish

Branding: drop the bespoke BRAND_TOKENS palette and bottom-reveal from
LandingShell (use the platform's own light tokens); re-ink the wordmark,
logo-mark, and hero loader from the gradient+glow to a solid --text-body
so the marks read as one ink with the nav text. Add a `shimmer` prop to
ThinkingLoader for a static --text-body label, and stroke the squeeze
arcs with the shared gradient.

Hero visual: the cursor now enters from below the field and chases the
send button live through the zoom (retimed beats, no arrive-then-wait);
the greeting fades in gently instead of shimmer-revealing; the click
ring becomes a press-dip (hero-cursor-press replaces hero-click-ring and
hero-greeting-reveal). Extract BlockHandles so the morphed GitHub card
carries a real edge handle in scene space; seed the compose card at its
true height; pop the sent bubble in immediately.

* improvement(landing): update feature iso-marks to perfected geometry

Re-author the four Mothership iso-mark illustrations (Integrate, Ingest,
Build, Monitor) on the refined isometric geometry, keeping the existing
animation vocabulary intact: hover line-draw plus per-mark auto-motion
(integrate float, ingest pulse, monitor panel-separate, build grid-flow).
Map the raw exports onto the shared token palette/line weight for
consistency and tune per-mark sizes for one optical weight.

Build is now pure CSS (grid-flow replaces the RAF wave), so it drops
'use client' and renders as a server component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): add pricing, privacy, terms, and changelog pages

- New public /pricing page: Free/Pro/Max/Enterprise cards with the full
  comparison breakdown transposed from shared upgrade data + JSON-LD; prices,
  CTAs, and features derive from shared billing constants so they can't drift
  from the in-app upgrade page.
- Migrate /privacy, /terms, and /changelog into the (landing) route group via a
  shared prose-page system (single source of truth for legal/prose chrome).
- Landing polish: solid-ink iso-mark illustrations + footer/cta/features/
  mothership spacing and token cleanups; sitemap adds /pricing.
- Audit pass: crawlable ChipLink CTAs, correct heading hierarchy, structured-data
  featureList derived from the visible comparison data, legal plan name Team->Max.

* large edits across landing finalization

* feat(auth): port OAuth-only signup + Microsoft provider from staging

Align auth-page logic with origin/staging (PR #5073) while keeping the
new chip-styled UI:

- Add Microsoft as a better-auth social sign-in provider (auth.ts) and
  surface it through the OAuth provider checker, providers API + contract,
  login/signup forms, SocialLoginButtons, and the landing auth modal.
- Gate email/password signup behind the emailSignupEnabled server flag
  (DISABLE_EMAIL_SIGNUP) so signup becomes OAuth-only when configured.
- Add DISABLE_MICROSOFT_AUTH / DISABLE_EMAIL_SIGNUP env + feature flags.

* fix(icons): render brand icons legibly when bare and on light tiles (#5292)

Monochrome brand icons hardcoded a single white or black fill matched to
their colored tile, so they vanished when rendered bare on the home
Suggested actions list (white-on-white in light mode, black-on-black in
dark mode). Convert those marks to currentColor so they adapt to context,
and make tile foregrounds contrast-aware via getTileIconColorClass instead
of a hardcoded text-white.

Also centralize all color math in apps/sim/lib/colors (perceived brightness,
hex/rgb/hsl conversion, contrast-text) and route every consumer through it:
the bare-icon audit, block tiles, logs trace view, whitelabeling theming,
workspace presence, and the PPTX renderer no longer carry duplicate copies.

Adds a bare-icon CI audit (scripts/check-bare-icons.ts) and authoring guidance.

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Waleed <walif6@gmail.com>
2026-06-30 13:50:04 -07:00
Waleed ca0a7ff0c2 feat(rich-markdown-editor): live media embeds + shared embed detection util (#5290)
* feat(rich-markdown-editor): live media embeds + shared embed detection util

- Extract getEmbedInfo/EmbedInfo into pure @sim/utils/media-embed (carries the
  PR #5288 dropbox host-validation hardening); repoint the note block to it
- Add LinkEmbed: a ProseMirror widget-decoration plugin that renders media
  players (YouTube, Vimeo, Spotify, Dropbox, …) beneath standalone links in the
  rich markdown editor, in both editing and read-only surfaces. The document
  stays a plain markdown link, so markdown round-trips stay lossless
- Gate embeds behind an opt-in flag (on for the file editor, off for modal fields)
- Polish the knowledge chunk editor to the file editor's centered reading frame
  while keeping it plaintext for exact embedding fidelity

* fix(media-embed): gate provider detection on parsed hostname

Validate each platform against the URL's parsed host before extracting, so a
look-alike host (youtube.com.evil.com) or a provider domain in the path
(evil.com/youtube.com/...) can no longer render a trusted-looking embed. Dropbox
is no longer a special case — all providers share the hostMatches gate. Also
consolidates the five Spotify branches and orders Twitch clip before channel.

* fix(rich-markdown-editor): unique widget key per duplicate embed URL

Key embed widgets by source + per-source occurrence index so two standalone
links to the same URL render as two distinct players instead of collapsing into
one, while keeping the key stable across unrelated edits (no iframe reload).

* refactor(media-embed): tighten comments and drop a redundant guard

- Drop the redundant paragraph type-check in getStandaloneLinkHref (the caller
  already filters to paragraphs) and rename the param for clarity
- Remove an inline comment and a TSDoc sentence that restated logic documented
  elsewhere
2026-06-30 11:27:42 -07:00
Waleed 7545391cb3 feat(docs): render workflow previews with the shared editor renderer (#5277)
* chore(workflow-renderer): declare @sim/emcn dep + wire the package into docs

Adds the missing @sim/emcn peer/dev dependency to @sim/workflow-renderer (it imports @sim/emcn in every View but resolved only via workspace hoisting). Wires apps/docs to consume @sim/workflow-renderer (dependency, transpilePackages, Tailwind @source) and adds remark-breaks (pulled transitively via the barrel's NoteBlockView export) — mirroring the @sim/emcn integration. Foundation for migrating the docs workflow-preview fork onto the shared Views. Build resolves the package/@source/remark-breaks cleanly.

* feat(docs): render loop/parallel containers with the shared SubflowNodeView

Replaces the forked PreviewContainerNode with a thin DocsContainerNode that maps the static preview data to SubflowNodeView's read-only (isPreview) props — no stores or hooks. Adds the block size to the preview node data so the view can size itself, and corrects the parallel example's start-edge handle id to 'parallel-start-source' (the view derives the handle id from kind). Deletes preview-container-node.tsx. Container colors/icons are now owned by the shared view (loop=blue, parallel=yellow).

* feat(docs): render block nodes with the shared WorkflowBlockView

Replaces the forked PreviewBlockNode with a thin DocsBlockNode that maps the static preview data to WorkflowBlockView's props — store-free, builds the subblock rows (condition/router Context+routes/default + tools + error) via SubBlockRowView, strips branch-id prefixes so the view's regenerated handle ids match, remaps router->router_v2, and keeps the framer-motion dim/stagger wrapper. Promotes resolveIcon into block-icons.tsx, adds the --workflow-edge token to docs global.css, deletes preview-block-node.tsx. The canvas diagrams now render with the real editor's view.

* refactor(workflow-renderer): make editor-only WorkflowBlockView props optional

The child-deploy, schedule, and webhook badge props (and their callbacks) only matter in the editor. Mark them optional and optional-chain the three callbacks so read-only consumers (docs, academy) can omit the whole group instead of passing ~18 explicit off-values. The editor still passes them, so its behavior is byte-identical (verified: apps/sim type-check clean). DocsBlockNode drops the off-props.

* feat(docs): replace how-it-runs static diagrams with live WorkflowPreview

Swaps the four static PNGs on the how-it-runs page for live, app-styled WorkflowPreview diagrams (concurrency, combination, condition+router branching, error path). Adds the four example workflows and renders error-port edges red to match the editor. The English page only; the translated execution/basics pages keep the PNGs.

* refactor(workflow-renderer): the view owns condition/router/error rows

Both the editor container and the docs adapter hand-built the condition/router/error summary rows in an order that had to stay in lockstep with the view's absolute handle-offset math — a three-way coupling with nothing enforcing it. The view now renders those rows itself from the conditionRows/routerRows it already receives (plus a routerContextValue prop for the router's Context row), so row order and handle geometry live together in one place. Both containers pass only data and their non-branch rows.

Editor is byte-identical: getDisplayValue moves to where conditionRows/routerRows are built; the no-subBlock SubBlockRow path is already an exact SubBlockRowView(title, value) passthrough; the error row stays gated on shouldShowDefaultHandles. Verified apps/sim type-check clean. Docs now also renders the error row on condition/router blocks, which the real editor already did (shouldShowDefaultHandles is true for them) — an alignment fix.

* refactor(docs): drop the parallel --wp-* token layer for the app/emcn tokens

The workflow-preview ran a 25-token --wp-* mirror (22 were pure aliases of app tokens docs already defines) plus a .wp-scope wrapper class. Replaces every var(--wp-X) with its canonical app/emcn token (--wp-edge->--workflow-edge, --wp-highlight->--brand-secondary, badges->--badge-*, etc.), adds the one missing token (--divider), and deletes the .wp-scope blocks + class. Visually identical (aliases resolve to the same values); the preview now inherits the same design tokens as the shared views and the rest of the app instead of a hand-rolled parallel set.

* refactor(docs): adopt emcn Badge + dedup resolveIcon in workflow-preview

output-bundle's hand-rolled type badge (BADGE_COLORS + a styled span) becomes the emcn Badge (its green/blue/orange/purple/gray variants use the identical --badge-* tokens). resolveIcon, which had three copies, is now imported once from block-icons by output-bundle and block-inspector.

* refactor(docs): rebuild the preview inspector on emcn chip primitives

The lightbox inspector was a hand-rolled facsimile (raw divs + a CONTROL class string + inline dashed borders). It now composes from the same @sim/emcn primitives the live editor's sub-block controls wrap — ChipSelect/ChipInput/ChipTextarea(viewOnly)/ChipSwitch/ChipTag/FieldDivider/Label — so it reads as the real editor panel, fed example data (read-only, full opacity via readOnly/viewOnly, not greyed). Slider stays minimal (no emcn equivalent) but on app tokens. Props API and embedded/standalone modes unchanged.

* refactor(docs): render the block-reference hero through the shared View

Retires the hand-rolled BlockCard (a parallel reimplementation of WorkflowBlockView) and the BlockDisplaySpec data model. Each block hero is now a single-block PreviewWorkflow (block-display-workflows.ts) rendered through the same toReactFlowElements -> DocsBlockNode -> WorkflowBlockView pipeline as the diagrams, mounted in a minimal fitView ReactFlow (maxZoom 1.3, no canvas chrome). A single block can no longer drift from the canvas.

* fix(docs): define sim's type scale + align the preview inspector to the editor

Docs Tailwind v4 never defined sim's custom font sizes (text-small/caption/md/micro), so emcn components (Label, Badge, the shared views) fell back to inherited sizes — the inspector labels rendered huge. Adds the type scale to the docs @theme. Also aligns the inspector header to the real editor panel (surface-4 bar, size-[18px] rounded-sm icon, text-sm name) and removes the Connections section (and its now-dead prop/wiring).

* fix(docs): inspector shows the full field list + dragged positions persist

Inspector: shows the block type's full field list (from the reference data) with the example's values overlaid, so it reads like the editor panel instead of only the canvas summary rows. Drag: selecting another block no longer relayouts the canvas — node positions the viewer dragged are preserved across highlight/selection changes (only a different workflow relayouts).

* feat(docs): highlight <> references + env vars; hide Ask AI over the lightbox; respace blocks

Inspector text fields render the value with <...> block references and {{...}} environment variables highlighted in brand-secondary (a lean read-only port of the editor's formatDisplayText), in the canonical chip field chrome. The floating Ask AI widget is hidden while a preview lightbox is open. Plus the example-data respacing so the editor-faithful Error row no longer makes stacked blocks overlap.

* fix(docs): make per-type field templates match the real block registry

Audited every block type's field list (the source the inspector + block-reference heroes render) against apps/sim/blocks/blocks/*. Corrected drift to the registry's default-visible fields, titles, and order: agent gains Temperature; router gains Model; wait gains Async; schedule rewritten (default is Daily, not minutes); webhook_trigger expanded to its real default-visible set; human_in_the_loop notification title fixed. Provider-credential and advanced-mode fields stay hidden, matching the editor. Canvas diagrams keep their clean curated rows; the inspector now shows the full, real field list per the chosen clean-canvas/full-inspector split.

* improvement(docs): taller default preview height so respaced diagrams aren't shrunk

Bumps the default WorkflowPreview height 260->300 (the respaced, editor-faithful blocks are taller, so fitView was shrinking diagrams that relied on the default). The tall how-it-runs routing diagram gets 400.

* improvement(docs): zoomable inline preview + taller default + themed controls

The inline preview is now zoomable outside the lightbox: adds react-flow zoom/fit Controls (themed to the dark canvas chrome) and enables pinch-zoom, while keeping scroll-zoom off so the page still scrolls over the diagram. Pan-drag and click-block-to-inspect already worked. Default height 300->340.

* improvement(docs): click canvas to expand; click empty lightbox to deselect

Clicking the inline preview canvas opens the full lightbox; clicking empty space in the lightbox clears the selection, matching the real editor.

* improvement(docs): reveal inline zoom controls on hover only

The always-visible zoom controls felt heavy on the inline preview; they now fade in on hover (matching the expand button) and stay visible in the lightbox.

* improvement(docs): drop zoom controls on the inline preview

Inline preview keeps pinch-zoom, pan, drag, and click-to-expand; zoom buttons stay in the lightbox only.

* improvement(docs): remove zoom controls from the lightbox too

Both previews zoom via scroll/pinch and pan via drag; no on-canvas zoom buttons. Drops the Controls import and its theming CSS.

* improvement(docs): match the real canvas — flat background + editor edge geometry

Closes the last faithfulness gaps the audit found: removes the dot grid (the real editor hides its background — flat bg), aligns PreviewEdge to the editor's smoothstep math (borderRadius 8, offset 30) and 2px stroke (default + error edges), the selection ring to 1.75px, and minZoom to 0.1. Structural parity (blocks/handles/containers/colors/tokens) was already shared. Kept PreviewEdge rather than swapping to WorkflowEdgeView, which would clobber the docs-only highlight/dim/animate for no visual gain.

* improvement(docs): rebrand the docs assistant as 'Ask Sim', styled like the real chat input

Renames the floating assistant from 'Ask AI' to 'Ask Sim' (matching the platform's voice — you talk to Sim) and restyles the composer to mirror the home chat input: a rounded-2xl bordered field with the toolbar inside, and the same 28px circular send/stop button (the home's exact active/disabled colors + white/black arrow). Updates the lightbox hide-selector to the new label.

* improvement(docs): match Ask Sim message styling to the mothership chat

Aligns the user bubble (rounded-[16px] surface-5, text-base/primary, leading-23, max-w-85%) and the assistant markdown (text-base, 600 headings/strong, text-primary dashed-underline links, surface-5 code blocks) to the real mothership chat's user-message + chat-content treatment, instead of the prior generic text-sm rendering. The composer already mirrors the home user-input (rounded-2xl field + 28px circular send button).

* improvement(docs): compact single-row Ask Sim composer

The two-row layout left a tall dead gap (the docs widget has no toolbar buttons to fill the second row). The composer is now a single row — textarea with the circular send button inline — so it sits at the natural input height.

* fix(docs): pass the router Context value to the shared view

DocsBlockNode never set routerContextValue, so the view (which renders the router's Context row from that prop, not from rows) showed a blank Context even when the preview data authored a value like <start.input>. Extract it from the block's Context row and pass it through.

* fix(docs): don't apply a block-type field template that doesn't match the block

inspectorFieldsFor keyed the full field template purely off block.type, but some types are reused across roles (a table action block vs the table trigger, a webhook trigger vs the webhook action), so the wrong template was applied. Only use the template when the block's authored rows are actually a subset of it; otherwise fall back to the block's own rows.

* fix(docs): connect preview edges to subflow container handles

toReactFlowElements hardcoded targetHandle to 'target' and defaulted source handles to 'source', but Loop/Parallel containers (SubflowNodeView) expose a 'loop-end-source'/'parallel-end-source' output handle and a left input handle with no id. Edges into and out of containers therefore failed to connect. Resolve each edge end to the block's real handle based on whether it's a container.

* fix(docs): don't expand the inspector template for blocks with no rows

block.rows.every(...) is vacuously true for an empty rows array, so a block defined only by branches (e.g. a router in ROUTING_WORKFLOW) inherited the type template's invented field defaults. Require non-empty authored rows before applying the template.

* fix(docs): render blank branch/router-context values as '-' like the editor

The editor maps condition/router branch values and the router Context through getDisplayValue, which renders '-' for a blank value. DocsBlockNode mapped them to an empty string, so else branches and unset routes looked blank instead of matching the editor. Mirror getDisplayValue's empty-value handling.

* fix(docs): show '-' for blank inspector branch values, matching the canvas

inspectorFieldsFor passed raw branch.value into the lightbox branch fields, so an unset else route read blank in the inspector while DocsBlockNode (and the editor's getDisplayValue) render '-' on the canvas. Normalize the same way; drop the now-redundant placeholder.
2026-06-29 20:27:33 -07:00
Waleed 27b2a4f585 feat(workflow-renderer): extract edge, subflow, and note Views into @sim/workflow-renderer (#5263)
Adds a @sim/workflow-renderer package with pure, props-driven WorkflowEdgeView, SubflowNodeView, and NoteBlockView shared by the editor and (future) docs preview. Moves block-dimensions constants into the package. Each editor node becomes a thin Container that wires stores/permissions and injects the editor-only ActionBar via a slot. No optimizePackageImports for the workspace component packages (avoids the toast-style module duplication); Tailwind scans the package source.
2026-06-29 11:45:05 -07:00
Waleed bcf6a804f9 improvement(emcn): extract design system into shared @sim/emcn package (#5257)
Moves apps/sim/components/emcn into a shared @sim/emcn package consumed directly by apps/sim and apps/docs. cn/keyboard/use-copy-to-clipboard move into the package; all imports become direct @sim/emcn (icons via @sim/emcn/icons, CSS via file path). ChipModal email validation is now prop-driven (quickValidateEmail stays in apps/sim, injected via validate). Docs drops its local chip/chip-dropdown/dropdown-menu mirrors and consumes @sim/emcn.
2026-06-28 22:50:48 -07:00
Vikhyath Mondreti c59631698f chore(deploy): remove deploy as a2a (#5255)
* chore(deploy): remove a2a

* add block
2026-06-28 20:01:24 -07:00
Vikhyath Mondreti 5a8134119a feat(workspaces): fork + push/pull (#5210)
* feat(workspaces): fork + push/pull

* type fix

* fix tests

* progress on ux

* remove modal section

* improve UI of modal

* update more ui

* make rollback part of the footer

* track skipped count correctly

* address comments

* make it workspace admin level

* update skipped count

* address more comments

* deal with unbounded memory possibility

* fix deleted kb article bug

* no deployed workflow case

* UI/UX cleanup

* fix oauth dropdown case

* fix oauth selector issue

* infra work + activity log

* consolidate migration

* update modal state

* more UI simplification

* grammar

* update audit report ui

* perf improvements

* fix tool input scenarios and add dependsOn UI handling

* minor comments

* fix webhook stability issues + drift detection removal

* make dependsOn subblock mapping cleanly stored

* fix: harden fork dependent-value mapping (clear stale rows, identity-guard first-sync fallback, perf + cleanup)

* address comments

* update comment

* enforce admin perms for activity api

* fix required + dependsOn combo
2026-06-28 13:13:57 -07:00
Waleed ca4e07baf3 chore(deps): bump undici to 7.28.0 and nodemailer to 9.0.1 (#5218)
* chore(deps): bump undici to 7.28.0 and nodemailer to 9.0.1

* chore(deps): dedupe cheerio and e2b transitive undici to 7.28.0
2026-06-25 17:33:06 -07:00
Will ChenandClaude Opus 4.8 6355c8e699 improvement(docs): Ask AI chat grounded in the docs vector store (#5172)
* docs: Ask AI chat grounded in the docs vector store

Adds an Ask AI chat to the docs site. A floating launcher opens a chat panel
backed by the Vercel AI SDK (OpenAI provider, OPENAI_API_KEY from the
environment). A searchDocs tool runs locale-scoped vector/keyword search over
the existing docs embeddings so answers cite real pages.

The public endpoint is hardened: per-request size/token/step caps, message
sanitization (no client-injected tool results or system prompts), origin
checks, and a per-IP rate limit. Non-English retrieval uses keyword search;
English vector search applies a similarity threshold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: harden Ask AI retrieval + fix stale loading state

- searchDocs: wrap the keyword query in try/catch too, so each retrieval path
  (keyword, vector) is independent best-effort
- ask-ai: gate the loading ellipsis to the in-progress (last) message so older
  empty bubbles don't re-show it while a later request streams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:32:29 -07:00
Theodore Li e1c3c7f6c9 feat(secrets): ingest env secrets at container runtime instead of fanning into ECS taskdef (#5189)
* feat(secrets): ingest env secrets at container runtime instead of fanning into ECS taskdef

The app/socket ECS taskdefs were ~42KB, ~93% of which was the secrets[] array:
268 pointer entries each restating the full ~78-char secret ARN, marching toward
the 64KB taskdef limit and growing ~150 bytes per hosted key added. The secret
blob itself is only ~18KB/268 keys.

Move secret delivery to container boot: new @sim/runtime-secrets loadRuntimeSecrets()
reads SIM_ENV_SECRET_ID, fetches the combined secret once, and hydrates process.env
(no-clobber, no-op when unset, fail-fast). Bootstrap entrypoints for app + realtime
await it before importing the real server (env-flags reads env at module load). The
app bootstrap is bun-bundled in the Dockerfile builder stage since it runs outside
the Next standalone bundle; realtime keeps full node_modules and runs the TS entry.

Backward-compatible: with the current fan-out taskdef the loader no-ops and the app
reads the injected env vars unchanged. The matching infra change (empty secrets[] +
SIM_ENV_SECRET_ID) ships separately, after this image is live.

* fix(runtime-secrets): address review feedback

- Move the binary-secret guard outside the retry loop (sendWithRetry) so a
  missing SecretString throws immediately instead of burning 3 attempts + backoff.
- Bound each Secrets Manager request with AbortSignal.timeout(5s) so a stalled
  response can't hang boot indefinitely.
- Drop the redundant @aws-sdk/client-secrets-manager pin from apps/realtime; it
  resolves transitively via @sim/runtime-secrets.
- Add a test for the non-retriable binary-secret path.
2026-06-24 16:23:37 -04:00
Theodore Li 0191a614b6 feat(pii): build & own combined PII (analyzer + anonymizer) image (#5176)
* feat(presidio): build & own combined analyzer+anonymizer image

Replace the stock mcr.microsoft.com/presidio-* sidecar images with a single
image we build and push to ECR/GHCR. A thin FastAPI service constructs one
AnalyzerEngine + one AnonymizerEngine at startup and serves both on port 3000
(/health, /supportedentities, /analyze, /anonymize) so the app needs one
PRESIDIO_URL. English only; pinned presidio 2.2.362 + en_core_web_lg 3.8.0.

Bakes in the native check-digit VIN recognizer and registers 12 English
recognizers Presidio ships but does not load by default (UK_NINO, AU_*, IN_*,
SG_*), taking the supported English set from 19 to 32.

* feat(presidio): add multi-language support (es/it/pl/fi)

Configure a multi-language spaCy NLP engine (en/es/it/pl/fi lg models) and
explicitly register the national-id recognizers Presidio ships but does not
load by default: ES_NIF/NIE, IT_FISCAL_CODE/DRIVER_LICENSE/VAT_CODE/PASSPORT/
IDENTITY_CARD, PL_PESEL, FI_PERSONAL_IDENTITY_CODE. Verified the NLP-engine +
explicit-registration path detects in-language (Finnish id, score 1.0).

* improvement(presidio): address review feedback

- Register VIN under all served languages, not just en (Bugbot: VIN missed for
  non-English language routing).
- Bump HEALTHCHECK start-period to 180s — five lg models load at import (Bugbot).
- Drop --no-cache-dir so the pip cache mount actually works (Greptile).
- Pydantic request models for /analyze + /anonymize so missing 'text' returns
  422 not 500; default operator 'type' to 'replace' instead of KeyError->500
  (Greptile).

* refactor(pii): rename presidio image artifacts to pii

Rename the image/repo/secret/files from 'presidio' to 'pii' for clarity — the
service does PII detection + anonymization (and backs the guardrails block's
block/mask), not just redaction, and 'pii' matches existing pii-* naming.

docker/presidio.Dockerfile -> docker/pii.Dockerfile
docker/presidio/ -> docker/pii/
ghcr.io/simstudioai/presidio -> .../pii
ECR_PRESIDIO secret -> ECR_PII (infra side already renamed)
No behavior change — paths/identifiers only.

* refactor(pii): move service to apps/pii, make image ECR-only

- Move server.py + requirements.txt from docker/pii/ to apps/pii/ (source belongs
  under apps/, matching app/realtime; Dockerfile stays in docker/). Add a minimal
  @sim/pii package.json so the apps/* bun workspace glob accepts the Python service.
- Repoint docker/pii.Dockerfile COPY paths to apps/pii/; rename the container user
  presidio -> pii.
- Drop GHCR for pii — it's a private ECS sidecar pulled from ECR, never published.
  Removed it from the arm64/manifest (GHCR-only) jobs and guarded the build-amd64
  tag step to skip GHCR when no ghcr_image is set.
2026-06-23 03:41:35 -04:00
Vikhyath Mondreti 633391903d feat(pi): add pi coding agent harness (#5178)
* feat(pi): add pi coding agent harness

* formatting

* update docs

* change version num

* guard to prevent prs on error

* update param visibility

* address security concerns

* fix tests

* reorder:
2026-06-22 21:47:53 -07:00