Commit Graph
5360 Commits
Author SHA1 Message Date
Waleed bbff34a43e fix(mcp): route object params to JSON editor, keep invalid drafts out of tool args (#5570)
* fix(mcp): route object-typed params to JSON editor, keep invalid drafts out of tool args

Two follow-up gaps from the earlier MCP schema fix:

- getInputType only routed array-typed and non-primitive-enum params to
  the long-input JSON editor; a plain object-typed param (no enum) fell
  through to the default short-input, which stores raw text via
  toString() and never round-trips a real object.

- The long-input onChange fell back to storing the raw typed text
  whenever JSON.parse failed (needed to keep the controlled textarea
  responsive mid-edit), but that meant an incomplete/invalid edit
  (e.g. `{"a":1` before the closing brace) could persist into the
  actual tool arguments. If executed in that state, the MCP execute
  route's array-coercion step would silently wrap the malformed string
  into a corrupted array instead of failing validation.

Invalid-edit text now lives in local `invalidJsonDrafts` state, keyed
by param name, instead of the real argument store — the textarea still
reflects every keystroke, but the persisted tool argument is always
either the last successfully parsed value or untouched. Drafts reset
when the selected tool changes.

* fix(mcp): reset invalid JSON drafts on schema change, not just tool change

The draft reset only fired when the selected tool id changed, so a
same-tool schema refresh (e.g. re-discovering tools from the live MCP
server) could leave a stale invalid draft displayed under a param name
whose shape had since changed. Key the reset off both the tool id and
a signature of the effective schema's properties, so any change to
what's actually being edited clears stale drafts.

* fix(mcp): key draft reset off both schema sources, not the resolved toolSchema

toolSchema resolves to cachedSchema || selectedToolConfig?.inputSchema,
so a live-only schema refresh (the discovered tool's inputSchema
changes but the cached _toolSchema snapshot doesn't) left the reset
key unchanged and could keep a stale invalid draft on screen. Track a
signature of each schema source independently so a change in either
one clears drafts, regardless of which source toolSchema resolves to.

* fix(mcp): sign whole schema for draft reset; invalidate drafts on external value changes

Two more follow-up gaps:

- schemaSignature only serialized schema.properties, so a same-tool
  refresh that changed only top-level fields like `required` (properties
  byte-identical) left the reset key unchanged. Sign the entire schema
  instead of cherry-picking fields, so nothing schema-level can be missed.

- A draft only reset on tool/schema change, so if the persisted argument
  changed for any other reason (undo/redo, a diff baseline switch, a
  collaborator's concurrent edit), the draft could keep shadowing the
  now-current value in the editor while execution used the real one.
  Drafts now carry a baseline signature of the value they were typed
  against; a draft only displays while that baseline still matches the
  live persisted value, so any external change makes it fall back to
  showing the real value instead of stale text.

* fix(mcp): restore comma-separated array input; stop spurious draft reset on tool load

Two more follow-up gaps:

- Holding every JSON.parse failure in a local draft blocked the
  documented comma-separated array shorthand (see the placeholder text)
  from ever reaching toolArgs, since plain comma-separated text is
  never valid JSON. Only an in-progress JSON array/object literal
  (starting with `[` or `{`) needs to stay in the draft until valid;
  plain array-typed text that isn't attempting JSON persists
  immediately as before, letting the execute route's existing
  comma-split/wrap coercion handle it as designed.

- draftResetKey always included the live selectedToolConfig schema
  signature, even when cachedSchema wins the `toolSchema` resolution.
  That segment flips from empty to populated the moment mcpTools
  finishes an unrelated async load, wiping in-progress drafts though
  neither the rendered schema nor the stored args changed. The live
  signature now only factors into the key when there's no cached
  snapshot for toolSchema to prefer.

* fix(mcp): reset drafts on genuine live schema refresh, not just its first load

Excluding the live schema signature whenever a cached snapshot exists
(the prior fix for a Cursor finding about mcpTools' initial load
spuriously wiping drafts) went too far the other way: a genuine
same-tool live schema refresh while a cached snapshot is still present
would no longer reset drafts either.

Track the live schema signature unconditionally, but only treat a
change as a real reset trigger when it goes from one non-empty
signature to a *different* non-empty one. The bare empty → non-empty
transition (mcpTools completing its initial fetch) is excluded, since
that's not a schema change; a populated → differently-populated
transition (an actual re-discovery) still resets drafts regardless of
whether a cached snapshot is present.

* fix(mcp): compare live schema against last-known-non-empty, not prior render

Comparing the live schema signature only against the immediately
preceding render's value meant a schema that dropped to empty and then
reappeared with different content was invisible to the reset check —
both the drop (X → '') and the reappearance ('' → Y) look like a bare
empty/non-empty transition, which was deliberately excluded to avoid
resetting on mcpTools' initial load. Track the last non-empty value
actually observed instead, so a transient empty gap no longer erases
the baseline: the schema reset now fires correctly when the tool
reappears with a genuinely different schema, while a real first-ever
load (no prior non-empty value at all) still doesn't spuriously reset.

* fix(mcp): re-baseline live schema tracker fresh on every tool switch

lastNonEmptyLiveSchemaSignature carried over across a tool switch
whenever the newly-selected tool's live schema hadn't loaded yet in
that same render (still empty). When it loaded a moment later, the
comparison was against the *previous* tool's signature, so the new
tool's first schema load could be misread as a "genuine refresh" and
wipe drafts the user had already started typing against the new tool.
A tool/cached-schema change now always re-baselines the live-schema
tracker to the new tool's current signature (even if still empty), so
the "same tool" refresh comparison never bleeds across tool switches.
2026-07-10 14:37:39 -07:00
Waleed ea4f2691e3 fix(providers): correct max-tokens param and add schema guidance for NVIDIA/Z.ai (#5569)
* fix(providers): correct max-tokens param and add schema guidance for NVIDIA/Z.ai

- NVIDIA NIM and Z.ai both document max_tokens for output-length control,
  not OpenAI's newer max_completion_tokens - the latter was silently
  ignored by both vLLM-served NIM models and Z.ai's GLM models
- Z.ai has no json_schema response_format mode (only text/json_object),
  so structured-output requests now also inject the expected schema into
  the system prompt as best-effort guidance, since the request param
  alone can't enforce field names/types

* style(providers): replace inline comments with TSDoc, per CLAUDE.md

Consolidated the scattered narrative // comments in nvidia/index.ts and
zai/index.ts into a single TSDoc block per provider documenting the
provider-specific API quirks; removed the rest where they only restated
what the code already shows. Also dropped an inline comment on zai's
modelPatterns field in models.ts.

* fix(providers): scope Z.ai schema guidance to the response_format call only

- schemaGuidance now falls back to the bare responseFormat object when
  .schema is absent, matching the schema-or-format fallback used
  elsewhere in the codebase (was silently injecting nothing for callers
  that pass a bare JSON schema)
- guidance is now only appended to the messages sent alongside an
  actual response_format (the immediate call, or whichever pass
  deferResponseFormat applies it to) instead of every turn of an
  active tool loop, where it wrongly told the model to return final
  JSON instead of continuing to call tools
2026-07-10 14:20:57 -07:00
Theodore LiandClaude Opus 4.8 97bb727eeb fix(pii): install CUDA torch on amd64 so GLiNER can run on GPU (#5552)
* fix(pii): install CUDA torch on amd64 so GLiNER can run on GPU

The published pii image installed a CPU-only torch build, so GLiNER on the
ECS GPU fleet died at model load with "Attempting to deserialize object on a
CUDA device but torch.cuda.is_available() is False". The Dockerfile already
had a TORCH_INDEX_URL arg, but no CI job ever passed --build-arg, so every
image silently took the cpu default.

Select the wheel index from TARGETARCH instead: amd64 gets cu128, arm64 keeps
the cpu index (cu128 publishes no aarch64 wheel at 2.11.0, and no arm64 target
has a GPU). CUDA torch falls back to CPU when no GPU is present, so one image
still serves both the Fargate CPU tasks and the EC2 GPU tasks off the same tag
— no CI or CDK changes needed.

cu128 keeps sm_75, the compute capability of the fleet's T4s, and its CUDA 12.8
runtime needs driver >=525 via minor-version compatibility, which the ECS GPU
AMI satisfies. cu121 was not an option: that index stops at torch 2.5.1.

Verified in an amd64 build of the changed block:
  2.11.0+cu128  cuda=12.8  arch=sm_75 sm_80 sm_86 sm_90 sm_100 sm_120
arm64 still resolves to 2.11.0+cpu. A build-time assert now fails the image
if amd64 ever silently regresses to a cpu wheel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHNEWVrh7k89m8Wtqzhs18

* fix(pii): assert torch CUDA state after every pip install

The check sat directly after the torch install, but requirements-gliner.txt
and requirements-dev.txt are installed afterwards and resolve against PyPI
with no torch pin, so a future gliner bump could swap the wheel that
torch_index selected without tripping the assert.

Neither file changes torch today (verified: torch is 2.11.0+cu128 both before
and after the gliner install), so this guards the invariant rather than fixing
a live regression. Moving it below the last pip install makes it certify the
torch that actually ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHNEWVrh7k89m8Wtqzhs18

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:21:45 -04:00
Waleed f0d85cb7ab fix(mcp): fix caret misalignment and tool schema contract validation (#5566)
* fix(mcp): fix caret misalignment in Add MCP Server modal fields

The Server URL and Header fields render a transparent input under a
formatted overlay div for env-var highlighting. The overlay used
font-medium/font-sans but the real input didn't, so glyph widths
diverged and the native caret drifted from the visible text as you
typed.

* fix(mcp): loosen tool schema contract to accept valid JSON Schema shapes

discoverMcpToolsContract's property schema rejected legal JSON Schema
that real MCP servers can return: array-form `items` (tuple
validation) and non-primitive `enum` values. Any server exercising
either shape failed contract validation client-side and blanked the
entire MCP tools list.

* fix(mcp): only render dropdown UI for primitive-valued enums

The MCP dynamic-args dropdown stringifies enum members for its
labels/values. Now that the tool schema contract accepts
non-primitive enum members (object/array), routing those through the
dropdown would collapse distinct values to "[object Object]" and
submit that string as the tool argument. Gate the dropdown on
primitive-only enums; non-primitive enums fall through to the
existing type-based branching (the JSON long-input editor for
object/array types), which round-trips arbitrary JSON correctly.

* fix(mcp): route non-primitive enums to the JSON editor regardless of type

isPrimitiveEnum() correctly excluded object/array enum members from
the dropdown, but the fallback only reached the long-input JSON
editor when paramSchema.type was 'array'. An object-typed (or
untyped) param with a non-primitive enum fell through to the default
short-input, which stringifies via toString() and drops the
enum-membership guarantee entirely. Any non-primitive enum now routes
straight to long-input, independent of the declared type.

* chore(mcp): fold inline comment into the existing TSDoc block

* fix(mcp): serialize non-string values before displaying in the long-input editor

The long-input JSON editor received value={value || ''} unconditionally,
so an argument already holding a parsed object/array (loaded from the
block's JSON arguments field) rendered as "[object Object]" or a
comma-joined list instead of valid JSON, and saving would overwrite the
real value with that mangled text. Serialize non-string values with
JSON.stringify before display; onChange still stores the raw text the
user edits, unchanged.

* fix(mcp): parse JSON-typed long-input edits back into real values

The long-input editor's onChange always stored the raw typed text, so
a param whose schema requires an object/array/non-primitive-enum value
(e.g. entering {"mode":"strict"}) was persisted as a string, not the
actual JSON value — the MCP tool call could receive the wrong type.
requiresJsonValue() identifies these schemas; onChange now parses the
edited text back into the real value once it's valid JSON, falling
back to the raw string mid-edit so the controlled textarea keeps
reflecting in-progress keystrokes.
2026-07-10 12:54:19 -07:00
Waleed 4952ddb73f feat(landing): add HubSpot tracking script for hosted marketing site (#5565)
* feat(landing): add HubSpot tracking script for hosted marketing site

- Loads the HubSpot loader in the landing route group only, gated by isHosted
- Not loaded for self-hosted/OSS deployments
- Adds the loader's companion scripts (analytics, form-tracking, banner) and their beacon hosts to CSP, verified against the actual scripts' network calls

* fix(landing): scope HubSpot CSP hosts to the landing route group only

Greptile flagged that the HubSpot script/connect hosts were added to the
shared CSP arrays used by every route, including /workspace, /login, and
/signup — even though the HubSpot loader only ever renders inside the
(landing) route group.

- Move the HubSpot hosts out of STATIC_SCRIPT_SRC/STATIC_CONNECT_SRC
- Add generateLandingRuntimeCSP(), which extends the shared runtime policy
  with the HubSpot hosts, mirroring the existing getChatEmbedCSPPolicy()
  pattern for route-scoped CSP variants
- Wire it into proxy.ts's catch-all branch, which is what actually serves
  the marketing/landing site; /workspace, /login, /signup keep the
  unmodified shared policy

* fix(landing): track HubSpot pageviews on client-side landing navigations

Cursor Bugbot flagged that the HubSpot loader only auto-fires a pageview
on the initial load. Since LandingLayout persists across client-side
navigations between landing routes, subsequent Link navigations never
told HubSpot about the route change, undercounting pageviews.

Add HubspotPageViewTracker, a small client component using the standard
Next.js App Router pattern (usePathname/useSearchParams in a Suspense
boundary) to push a manual pageview through HubSpot's _hsq queue on every
navigation after the first.

* simplify(landing): drop unnecessary Suspense from HubSpot pageview tracker

usePathname() alone doesn't require a Suspense boundary to preserve static
rendering — only useSearchParams() does. The tracker only needs the path,
not the query string, so drop useSearchParams and the Suspense wrapper
entirely. Simpler, and no risk to the landing site's static rendering/LCP.

* fix(landing): exclude non-landing routes from the landing CSP fallback

Greptile's second pass caught that proxy.ts's catch-all branch (which
serves generateLandingRuntimeCSP()) also handles several non-landing pages
that fall through the earlier explicit branches: /verify, /sso,
/reset-password (auth sub-pages), /resume/[workflowId] (interfaces),
/f/[token] (file shares), /playground, and the authenticated/callbackUrl
invite fallthrough. None of these render the HubSpot loader, so they
shouldn't get its CSP allowance either.

Add an explicit non-landing path prefix list and only fall back to
generateRuntimeCSP() (the tight policy) for those, keeping
generateLandingRuntimeCSP() for everything else in the catch-all.

* fix(landing): add /unsubscribe to non-landing paths, fix tracker remount bug

- Greptile correctly flagged /unsubscribe as another top-level page outside
  (landing) that reaches the CSP fallback branch; add it to the exclusion list
- Cursor caught that the per-mount useRef in HubspotPageViewTracker resets
  whenever LandingLayout remounts (e.g. leaving the landing site and coming
  back), but next/script dedupes the loader by id and won't re-fire the
  auto-tracked pageview on remount — so that return visit was silently
  dropped. Move the flag to module scope so it reflects the actual
  once-per-browser-session lifetime of the loader script, not per-mount

* fix(landing): track query-only landing navigations in HubSpot pageviews

Cursor caught that the tracker only depended on usePathname(), so
client-side navigations that change only the query string (blog/library
pagination, careers filters) never fired a pageview at all, and setPath
dropped the search string even when the path did change.

Add useSearchParams() back (the officially documented Next.js pattern for
tracking all route changes) and depend on the full path+query string.
Wrap the tracker in a local Suspense boundary, as required to keep the
route statically rendered — the fallback is null and the component
renders nothing, so this has no LCP/visual cost.

* test(proxy): add regression coverage for the non-landing path classifier

Exports isNonLandingPath and covers the exact prefix-boundary cases
(e.g. /f vs /ffoo, /resume vs /resumes) that the CSP routing fix depends
on, so this logic is verified by CI rather than my own ad-hoc checks.

* fix(landing): exclude /landing-preview from the landing CSP fallback

Greptile caught that /landing-preview calls notFound() in production
(see app/landing-preview/page.tsx) and its subroutes (marks-lab,
readme-tour-capture) don't render the (landing) layout either, so none of
them ever load the HubSpot tracker — but the CSP fallback was still
classifying the whole prefix as landing.

* chore(landing): remove the /landing-preview test scaffold

It was a temporary route for visual iteration (404s in production) — not
needed anymore, and Greptile had just flagged it as another route that
falsely inherited the landing CSP allowance. Removing it outright is
simpler than maintaining an exclusion for it.

Also removes SandboxWorkspacePermissionsProvider, which existed solely to
support the deleted readme-tour-capture page and has no other callers.

* refactor(landing): fix invalid const assertion, trim comments

- Fixed HUBSPOT_SCRIPT_SRC/HUBSPOT_CONNECT_SRC: 'as const' cannot wrap a
  ternary expression directly (TS1355) — caught by a full project typecheck
  I ran specifically to verify this PR, not by lint/tests alone. Rewrote
  using the same conditional-spread-inside-array-literal pattern already
  used by every other array in this file (e.g. STATIC_FRAME_SRC)
- Trimmed comments across all four touched files down to only the
  non-obvious 'why' (module-scope tracking flag, HubSpot CSP scoping
  rationale), matching this codebase's terse comment style elsewhere

* revert(landing): drop landing-scoped CSP, put HubSpot in the shared policy

Cursor caught a fundamental problem with the landing-scoped CSP: the
Content-Security-Policy header is fixed to the document's initial HTTP
response and is NOT re-applied on Next.js client-side (soft) navigation.

Both the landing navbar's ChipLink to /login and AuthShell's Link back to
/ are soft navigations (confirmed directly in the source, not assumed).
That means:
- /login -> / (soft nav): the browser keeps /login's CSP, which never
  allowed HubSpot hosts, so the loader gets silently blocked on landing.
- / -> /login (soft nav): the browser keeps the landing CSP, which is
  MORE permissive than /login's, undoing the tightening entirely.

A per-route CSP is fundamentally incompatible with this app's
client-side-routed navigation. Greptile's original 'CSP too broad on
/workspace' concern was valid in isolation, but the fix built across the
last several rounds doesn't actually work — it's neither reliably
tighter nor reliably functional, and each round's patch (exclusion list
entries, /landing-preview handling) was really just papering over that
core issue.

Revert to a single shared CSP for the whole app, matching exactly how
GTM/GA/ahrefs are already handled in this same file: HubSpot hosts land
in STATIC_SCRIPT_SRC/STATIC_CONNECT_SRC under the existing isHosted
gate. /workspace's CSP header technically allows origins it never
requests (same accepted tradeoff as GTM/GA), but the tracking script only
ever renders in the (landing) layout — matching the CSP scope Greptile
originally objected to. This is now provably correct because it can't
desync: proxy.ts, csp.ts, and proxy.test.ts are byte-for-byte identical
to origin/staging except for the HubSpot host list itself.

* fix(csp): drop overbroad *.hubspot.com connect-src wildcard

Greptile correctly flagged that *.hubspot.com is far broader than
anything the tracker actually needs — it covers HubSpot's entire product
surface (app, api, marketing), not just the tracking endpoints.

Re-checked my own network trace from earlier: the pageview beacon itself
is an image pixel (new Image() to track.hubspot.com/__pto.gif), which is
governed by img-src (already wide open to any https: origin), not
connect-src. The *.hubspot.com entry was an unverified guess for the
forms-API/banner fetch calls I couldn't pin down through minification —
removing it since I can't confirm what it was actually protecting,
keeping only the verified *.hscollectedforms.net entry.
2026-07-10 12:24:57 -07:00
Waleed 08320d5ab0 fix(sidebar): fix rename input losing its selection on open (#5563)
* fix(sidebar): fix rename input losing its selection on open

Radix's FocusScope defers close-time focus teardown to a setTimeout(0),
which can occasionally run after the rename input's own focus()/select()
and clobber the selection. Focus the input from onCloseAutoFocus instead,
which runs inside that same deferred teardown and always wins the race.

Affects workflow, folder, and workspace rename (all route through the
shared sidebar ContextMenu component).

* fix(sidebar): only refocus the rename input when Rename triggered the close

onCloseAutoFocus fires on every menu close, not just after selecting
Rename. Gate the refocus behind a ref set only when the Rename item
was selected, so closing the menu for an unrelated action (Delete,
Duplicate, ...) while an earlier rename is still live doesn't steal
focus back into it and delay its blur-save.
2026-07-10 10:55:34 -07:00
Waleed 8bf942407c feat(models): add latest Groq and Cerebras models (#5561)
* feat(models): add latest Groq and Cerebras models, flag near-term retirements

- Groq: add qwen/qwen3.6-27b (Preview, $0.60/$3.00, 131k ctx) - live on
  console.groq.com/docs/models, not previously in the catalog
- Groq: mark meta-llama/llama-4-scout-17b-16e-instruct and qwen/qwen3-32b
  deprecated - both have an announced shutdown date of July 17, 2026 per
  Groq's own deprecations page
- Cerebras: add gemma-4-31b (Preview, $0.99/$1.49, 131k ctx/40k max output) -
  live on inference-docs.cerebras.ai, not previously in the catalog

Every field independently verified via 2+ live sources (provider docs +
pricing pages) before adding; no code changes needed since both providers
use generic OpenAI-compatible completions with no per-model special-casing.

* fix(models): add verified releaseDate for groq/qwen/qwen3.6-27b

Greptile correctly caught that omitting releaseDate causes this model to
sort last within the Groq section in the model picker (orderModelIdsByReleaseDate
treats a missing date as Number.NEGATIVE_INFINITY) - misleading since it's
actually the newest model in the lineup. Verified 2026-04-21 (Qwen's own
upstream release date, matching this repo's existing convention of using the
model creator's release date rather than a reseller-specific date) via
llm-stats.com, cross-checked against two other independent sources.
2026-07-10 09:58:34 -07:00
2986362b38 feat(providers): add NVIDIA NIM and Z.ai providers (#5560)
* v0.6.29: login improvements, posthog telemetry (#4026)

* feat(posthog): Add tracking on mothership abort (#4023)

Co-authored-by: Theodore Li <theo@sim.ai>

* fix(login): fix captcha headers for manual login  (#4025)

* fix(signup): fix turnstile key loading

* fix(login): fix captcha header passing

* Catch user already exists, remove login form captcha

* feat(providers): add NVIDIA NIM and Z.ai providers

- NVIDIA NIM (BYOK): Nemotron model family (70B/Ultra-253B/Super-49B v1.5,
  Nemotron-3 Nano/Super/Ultra) via integrate.api.nvidia.com's
  OpenAI-compatible API
- Z.ai (hosted): GLM model family (5.2 down to 4-32B) via api.z.ai's
  OpenAI-compatible API, bare glm-* model ids with no provider prefix,
  Sim-provided key rotation (ZAI_API_KEY_1/2/3) matching openai/anthropic/google
- Z.ai tool_choice is forced to 'auto' since the API only documents auto
  support; forced/none tool_choice from prepareToolsWithUsageControl is
  ignored with a warning log instead of being sent to the API

* fix(providers): scope zai model routing to the exact catalog

Drop the /^glm/ fallback pattern - it would overmatch any unrelated
self-hosted "glm-*" model (e.g. a custom vLLM/LiteLLM deployment) and
misroute it to Z.ai's hosted, Sim-billed key. Routing now relies solely
on the exact model-id match against zai's static catalog.

* fix(providers): wire thinking/reasoning_effort into Z.ai requests

request.thinkingLevel and request.reasoningEffort were computed but
never mapped onto the Z.ai payload, so the thinking toggle and GLM-5.2's
reasoning_effort control silently no-op'd and always ran on Z.ai's
server-side default instead of the user's selection.

* fix(providers): route Z.ai through the workspace BYOK resolver too

getApiKeyWithBYOK (the resolver actually used by workspace provider
runs, per providers/index.ts) only rotated server keys for
openai/anthropic/google/mistral, so GLM calls without a user apiKey
failed even with ZAI_API_KEY_1/2/3 configured — getApiKey in
providers/utils.ts had zai wired but that's not the codepath workspace
runs go through. Add zai to the hosted-check condition, and register it
as a BYOKProviderId so a workspace can also bring its own Z.ai key.

---------

Co-authored-by: Theodore Li <theodoreqili@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-07-10 09:15:42 -07:00
Waleed 7d1c927ab1 fix(models): correct model catalog data and Gemini thinking-config wire format (#5559)
* fix(models): correct model catalog data and Gemini thinking-config wire format

- OpenAI: remove fabricated 'max' reasoning-effort value from gpt-5.6 family
- Anthropic: fix claude-sonnet-4-6 maxOutputTokens (64k -> 128k); remove 3 fully
  retired models (claude-opus-4-0, claude-sonnet-4-0, claude-3-haiku-20240307);
  fix budget_tokens/max_tokens clamp that could send budget_tokens >= max_tokens
  for claude-opus-4-1 at its default thinking level
- Google/Vertex: un-deprecate gemini-3-flash-preview (no shutdown date announced);
  add thinking capability to gemini-2.5-pro/flash/flash-lite (google + vertex)
- Fix gemini/core.ts to send thinkingBudget (not thinkingLevel) for Gemini
  2.5-series models, which reject thinkingLevel entirely - only Gemini 3.x
  supports it
- Bedrock: mark claude-opus-4-1 deprecated per AWS's own Bedrock lifecycle
  schedule (Legacy since Jul 8 2026, separate from Anthropic's direct-API date)

* fix(models): restore retired Claude entries as deprecated instead of removing

Greptile caught a real backward-compat regression: fully removing
claude-opus-4-0/claude-sonnet-4-0/claude-3-haiku-20240307 dropped them from
getHostedModels()/shouldBillModelUsage(), so saved workflows still referencing
them would fail on a missing-API-key error instead of Anthropic's actual
"model retired" error. deprecated:true isn't consumed by the model picker
(only copilot's serializer reads it), so restoring them this way costs
nothing on hiding from new selection while preserving hosted-key resolution
for existing references.

* fix(models): restore Sol-exclusive 'max' reasoning value; fix Gemini 2.5 disable-thinking gap

Found during a final per-model audit round with independent 2-3 source
verification on every changed model:

- gpt-5.6-sol: restore 'max' reasoning-effort value. Multiple independent
  sources (OpenAI's own model-guidance docs page, launch announcement, and
  press coverage) confirm 'max' is a real, newly-launched value exclusive to
  Sol - not fabricated as originally assessed. Terra and Luna correctly do
  NOT get 'max' (confirmed Sol-exclusive), so they're unchanged.
- gemini-2.5-flash / gemini-2.5-flash-lite (google + vertex): selecting
  'none' for thinking level was sending no thinkingConfig at all, which
  falls back to the API's dynamic default (thinking stays ON for flash) -
  not actually disabling it, even though both models explicitly support
  thinkingBudget:0. Now sends an explicit budget of 0 for these two models
  specifically (gemini-2.5-pro is correctly excluded - it cannot disable
  thinking at all, floor is 128 not 0).

* chore(models): tighten inline comments in anthropic/gemini core

Trim verbose multi-line comments to single concise lines and remove
duplication with the TSDoc already on the ANTHROPIC_MIN_BUDGET_TOKENS/
ANTHROPIC_THINKING_OUTPUT_HEADROOM constants. No behavior change - verified
against the full test suite (811 files, 11141 tests, all passing).
2026-07-10 09:08:00 -07:00
Waleed 2cfa040f23 feat(brex): add transfer/budget/spend-limit/vendor write tools (#5558)
* feat(brex): add transfer/budget/spend-limit/vendor write tools

- fix get_company transformResponse reading camelCase accountType instead of Brex's account_type field (always null in prod)
- add brex_create_transfer, brex_create_budget, brex_archive_budget, brex_create_spend_limit, brex_create_vendor, brex_update_vendor
- endpoints verified against Brex's live payments_api.yaml and budgets_api.yaml OpenAPI specs
- guard required money-amount fields against blank/NaN input instead of silently coercing to 0

* fix(brex): correct status enum docs and normalize expense date filters

- get_budget/get_spend_limit output descriptions listed status enum values not in Brex's actual schema
- list_expenses now normalizes purchased_at_start/end through toBrexDateTime for consistency with list_card_transactions/list_cash_transactions

* fix(brex): preserve zero values and normalize booleans in write params

- toRequiredAmount now rejects whitespace-only input (Number(' ') coerces to 0)
- limitBufferPercentage/transactionLimitAmount used truthy checks that dropped explicit 0
- isPproEnabled now normalized to a real boolean instead of forwarding a stringified 'false' from dynamic references

* fix(brex): null-safe PPRO coercion and fail-fast on empty vendor update

- toOptionalBoolean now treats null the same as undefined (was only checking undefined), so a null isPproEnabled from a dynamic reference is omitted instead of coerced to false
- brex_update_vendor throws when no updatable field (companyName/email/phone) is provided, instead of sending an empty PUT body
2026-07-09 22:54:46 -07:00
Waleed f9a5d8b113 fix(security): close code-scanning and dependabot alerts (#5557)
* fix(security): close code-scanning and dependabot alerts

- markdown-paste.ts: strip <style>/<script> in a loop, not a single
  pass, so nested/overlapping tags can't leave a surviving <script>
  behind (incomplete multi-character sanitization)
- block-identity.ts: annotate the two SHA-1 uses as intentional
  (UUIDv5 per RFC 4122, deterministic id derivation only, not a
  security use of the hash) rather than swap algorithms, which would
  change every derived fork block id
- apps/pii: bump transformers 4.56.2 -> 5.3.0 (CVE-2026-4372 RCE via
  crafted config.json, CVE-2026-1839 RCE via Trainer torch.load),
  huggingface_hub 0.35.3 -> 1.3.0 (transformers 5.3.0's floor), and
  pytest 8.4.1 -> 9.0.3 (CVE-2025-71176 tmpdir handling); verified
  pip resolves cleanly and the unit test suite passes on 9.0.3

* fix(files): make markdown-paste sanitizer O(n) instead of O(n*depth)

Greptile flagged the repeated-replace loop from the prior commit: it
strips <style>/<script> correctly but rescans the whole string once
per nesting level, so deeply nested clipboard HTML can freeze the tab.
Replace it with a single linear pass that tracks nesting depth of the
open tag via a tag-token scan, dropping the element in one pass no
matter how deeply nested.

* style: fold inline comments into TSDoc per repo comment convention

Repo convention is TSDoc-only documentation, no non-TSDoc explanatory
comments. Moved the uuidV5 SHA-1 rationale and the stray-close-tag note
into the existing TSDoc blocks above each function. Left the two
lgtm[...] annotations as trailing comments since those are functional
CodeQL suppression directives (must sit on the flagged line), not
documentation.

* test(files): lock in nested-tag stripping regression for markdown paste

Covers the case Greptile flagged: nested and 50-deep <script> tags
must strip in one pass without leaking a dangling tag.

* fix(files): drop unterminated <script>/<style> instead of leaking it

Cursor Bugbot caught two related bugs in the single-pass rewrite: if
pasted HTML ends while a script/style element is still open (truncated
or malformed clipboard HTML), cursor never advanced past the open tag,
so the final flush re-appended the untouched tag/content (leaking an
unstripped <script>) and duplicated the prefix already copied into
result.

Fix: advance cursor the moment a tag opens, not when it closes, and
only do the final flush when we end at depth 0. An element that never
closes has cursor already past its open tag, so it and everything
after it is dropped instead of reappearing.
2026-07-09 20:54:29 -07:00
Waleed e2cb3b29ba fix(files): fix savingRef mutex integrity race after discard correction (#5554)
* fix(files): fix savingRef mutex integrity race, anchor discard un-suppress to captured target

An independent 4-agent audit (beyond the bot review loop) converged on a real
race in the round-11 discard-correction fix:

- save()'s deferred MIN_SAVING_DISPLAY_MS status timer resolves as a macrotask
  well after the save's own promise (used to sequence discard's correction)
  has already settled as a microtask. That timer unconditionally reset
  savingRef/triggered a trailing resave, even after discard's correction had
  since claimed the save slot for its own in-flight write — letting a fresh
  debounced save start concurrently with the correction. Now guarded with
  `if (inFlightRef.current) return` before touching savingRef, so it only
  acts when nothing else has claimed the slot since.

- The render-time un-suppress check keyed off isDirty, which a stale save's
  markSavedContent(next) landing after discard can transiently corrupt
  (overwriting savedContent with the pre-discard value while content has
  already been reverted), causing a spurious "genuinely new edit" signal.
  Now keyed off a discardTargetRef captured at discard time instead, which
  that corruption doesn't touch.

Also hardened recovery to key its once-per-mount guard on the specific
draft key rather than a bare boolean, so a hypothetical future caller that
reuses a hook instance across files would still get correct recovery
(today's real callers already remount per file, so this is defense in
depth, not a behavior change for any current caller).

* fix(files): fix discard status masking and cross-key discard suppression leak

Greptile round-1 review on the follow-up fix PR caught 3 real gaps in the
discard/correction flow:

- The display timer's !discardedRef guard (added to stop a stale save's
  status update from clobbering a running correction) also silently
  suppressed the idle-timer reschedule, and discard()'s own correction never
  set a terminal status on settle — so saveStatus could stick on 'saving'
  forever after a successful correction, and a failed correction surfaced
  only via the onDiscardCorrectionFailed callback with no status change.
  Now the correction's own .then()/.catch() sets 'idle'/'error' once it
  owns the flow (only when nothing has since un-suppressed discard).

- The discard-suppression un-suppress check compared content against the
  captured discard target, but never reset if a hook instance were reused
  across draftKeys — a coincidental content match with the previous file's
  target would keep discard permanently suppressing saves for the new file.
  Reset discardedRef whenever the effective draftKey changes.

* fix(files): re-chain autosave after a discard correction settles

Cursor Bugbot found the discard correction's finally() cleared the save
mutex but never rechecked for dirty content: an edit made while the
correction was in flight bailed out of the debounce effect (savingRef was
held) and, since content isn't a savingRef dependency, was never
rescheduled once the mutex freed — the edit could sit unsaved indefinitely.
The same gap explained a related report that a failed correction which had
already been superseded by a newer edit left saveStatus stuck, since
nothing else was driving it forward.

Fix is one line: call save() in the finally() when content is still dirty.
save()'s own guards (savingRef/discardedRef/content-equality) make this
safe to call unconditionally, and it naturally hands status ownership to
the newer edit's own save cycle.

* fix(files): key discard state to raw draftKey, make failed corrections retryable

Cursor Bugbot round 3 caught 2 more real gaps:

- The document-change reset added last round compared effectiveDraftKey
  (draftKey gated by enabled), so toggling enabled alone for the SAME
  document — e.g. a streaming lock — looked identical to switching files.
  That cleared discardedRef mid-correction, which skipped the correction's
  own setSaveStatus('error'/'idle') (gated on discardedRef to avoid
  clobbering a newer edit's status), silently stranding the hook on
  'saving' with no visible retry affordance. Now keyed off the raw
  draftKey, which enabled toggling never touches.

- After a failed correction, content already equals savedContent (that's
  what discard reverted to), so saveImmediately()'s retry going through
  save() hit its dirty-check and was a complete no-op — the error toast's
  Retry button did nothing. Extracted the correction logic into
  runCorrection(target), shared by discard() and by saveImmediately when a
  failed correction is pending, so retry pushes the reverted baseline
  again instead of bailing on a check that assumes retries are always for
  dirty content.
2026-07-09 20:04:48 -07:00
Waleed 896d15b666 fix(helm): close blocker/real-gap findings from Helm chart best-practices audit (#5555)
* fix(helm): close blocker/real-gap findings from Helm chart best-practices audit

Verified every finding against the official Helm docs and Kubernetes Pod
Security Standards docs before fixing, and validated each fix with
helm lint/template plus the chart's own helm-unittest suite (65 -> 79
tests, all new tests confirmed to fail on the pre-fix code):

- Blocker: values.schema.json documented "minimum 32/8 characters" on
  BETTER_AUTH_SECRET/ENCRYPTION_KEY/postgresql.auth.password but never
  enforced it. Added anyOf minLength-or-empty constraints (empty stays
  legal for existingSecret/ESO modes) — verified negative/positive cases
  live, no regression for any secret-delivery mode.
- Real gap: copilot didn't support the External Secrets Operator mode the
  rest of the chart offers (app/postgresql/externalDatabase). Added
  external-secret-copilot.yaml, remoteRefs.copilot, and extended
  sim.copilot.validate with the same "map it or remove it" fail-fast
  guard app.env/realtime.env already have. Verified byte-identical
  rendering for the existing non-ESO path.
- Real gap: the OpenTelemetry Collector was the only workload missing the
  shared Restricted-profile securityContext helpers (no container-level
  hardening at all). Wired sim.podSecurityContext/containerSecurityContext
  in, preserving the collector's original UID/GID/fsGroup.
- Real gap: copilot templates hand-rolled label/selector blocks instead of
  using the chart's established sim.<component>.labels/selectorLabels
  pattern. Added sim.copilot.*/sim.copilotPostgresql.* helpers and
  refactored every consumer — confirmed byte-identical helm template
  output before/after (selector labels are immutable on upgrade, so this
  was verified, not assumed).
- Documented (README): the ingressFrom default and readOnlyRootFilesystem
  posture, both real but intentional tradeoffs the audit flagged as
  underdocumented. Added extraVolumes/extraVolumeMounts to copilot's
  Deployment (realtime/pii already had it) so the readOnlyRootFilesystem
  guidance is actually actionable for all three stateless services.

Deferred (nice-to-have, not blocking): pinning the two floating Postgres
image tags, values.schema.json stubs for ~13 uncovered top-level sections,
and an OTel collector image version bump — none are correctness issues.

* fix(helm): move copilot's static config out of the ESO-required Secret

Greptile caught a real bug: copilot.server.env shipped with non-empty
static defaults (PORT, SERVICE_NAME, ENVIRONMENT, LOG_LEVEL), unlike
app.env/realtime.env which ship fully empty. The new ESO validation
correctly required every non-empty env key to be mapped in
externalSecrets.remoteRefs.copilot — but that meant a default install
with copilot + ESO enabled failed demanding secret-store paths for
values that were never secrets.

Fixed by applying the chart's own existing pattern for this exact
problem: moved the 4 static keys into copilot.server.envDefaults
(mirroring app.envDefaults) and inlined them as plain container env,
bypassing the Secret/ExternalSecret system entirely — same rationale
already documented for app.envDefaults. Verified live that Greptile's
exact repro (default copilot env + ESO enabled, only the 7 real secrets
mapped) now renders cleanly and the four values still reach the
container. Added a regression test that fails on the pre-fix code.

* fix(helm): don't shadow copilot's existingSecret with envDefaults

Greptile and Cursor Bugbot both independently caught this: in
copilot.server.secret.create=false (existingSecret) mode, the chart
still unconditionally inlined copilot.server.envDefaults as explicit
container env. Kubernetes gives explicit env precedence over envFrom,
so a pre-existing Secret's PORT/LOG_LEVEL/etc values were silently
overridden by the chart defaults — the exact shadowing bug
app.envDefaults already guards against via its own $useExistingSecret
skip, which I forgot to mirror when copying the pattern to copilot.

Skip envDefaults entirely in existingSecret mode (matching app's
existing behavior — the pre-created Secret is the sole source of
truth), while still rendering extraEnv. Verified live: existingSecret
mode now renders no env: block at all when extraEnv is unset, and
still renders extraEnv without envDefaults leaking in when it is set.
Added two regression tests, confirmed both fail on the pre-fix code.

* fix(helm): key copilot's existingSecret check off its own secret.create, not the global ESO flag

Round 2's fix (which I copied nearly verbatim from Greptile's own
suggested diff) used $useExistingSecret := and (not
externalSecrets.enabled) (not copilot.server.secret.create) — Greptile
caught its own suggestion's remaining bug on round 3: when
externalSecrets.enabled=true globally (for app/postgresql) but copilot
itself uses copilot.server.secret.create=false with its own
pre-created Secret, that condition evaluated to non-existingSecret mode,
so envDefaults still inlined and shadowed the user's Secret values —
same bug, different trigger condition.

envFrom always points at the user-provided Secret name whenever
secret.create=false, independent of what other components do with ESO,
so the check should key on that alone. Verified live: global ESO
enabled + copilot's own existingSecret now renders no env: block and
envFrom correctly points at the pre-created secret name; the two
scenarios that should still inline (copilot itself on ESO, plain
inline mode) still work. Added a regression test, confirmed it fails
against round 2's guard.

* fix(helm): checksum/secret annotation on copilot ignores ESO-sourced secret

Cursor Bugbot caught a real bug: checksum/secret only hashed
secrets-copilot.yaml's rendered output, but under
externalSecrets.enabled=true that template renders nothing (env
credentials come from external-secret-copilot.yaml instead). Result:
changing externalSecrets.remoteRefs.copilot mappings wouldn't change
the pod template hash, so Kubernetes would never restart the copilot
pod to pick up the new mapping — stale envFrom values until a manual
restart.

Fixed by hashing the concatenation of both templates' rendered output:
whichever mode is active, only one renders non-empty content, but the
concatenated hash still changes on a mode switch or a remoteRefs
change. This can't reach into the live secret store value ESO syncs
(Helm only sees the ExternalSecret manifest at render time) — that's
an inherent ESO limitation, not something a checksum annotation can
close; documented as such in the template comment.

Note: deployment-app.yaml and deployment-realtime.yaml have this same
latent limitation in ESO mode (checksum/secret only hashes
secrets-app.yaml), but that's pre-existing code outside this PR's
diff — not fixed here to stay scoped to what Cursor actually flagged.

Verified live: the checksum differs across two different
remoteRefs.copilot.LICENSE_KEY mappings, and still changes correctly
in plain inline mode. Added a regression test.
2026-07-09 20:00:52 -07:00
Theodore Li 5b7513f15d feat(blocks): add block visibility gating (preview blocks + AppConfig reveals) (#5526)
* fix(deps): install xlsx from @e965/xlsx npm mirror

The dependency was pinned to a direct tarball on cdn.sheetjs.com, which
now returns 403 (Cloudflare bot-challenge) to automated clients, breaking
bun install in CI. npm's own xlsx is frozen at 0.18.5, so switch to the
@e965/xlsx mirror which republishes the identical 0.20.3 CDN build to the
npm registry. No code changes needed — all imports use bare 'xlsx'.

* feat(blocks): add block visibility gating (preview blocks + AppConfig reveals)

* fix(blocks): reset visibility to fail-closed empty state on workspace switch

* fix(blocks): carry kill-switch entries across workspace-switch visibility resets

* chore(deps): revert stray local xlsx-mirror commit (keep staging's pinned source)

* chore(skills): rename gate-block skill to add-block-preview
2026-07-09 22:38:01 -04:00
Waleed a3487da8a1 improvement(files): remove Save UI in favor of silent autosave with local-first draft recovery (#5549)
* improvement(files): remove Save UI in favor of silent autosave with local-first draft recovery

Files editor no longer shows a Save/Saving/Save failed button - autosave
already ran in the background, the button was vestigial. Cmd+S still works.

Adds local-first draft persistence to the shared useAutosave hook (opt-in via
draftKey): edits mirror into IndexedDB on a 400ms debounce, independent of the
1.5s network save, and flush best-effort on visibilitychange/pagehide. On
reopen, a newer local draft is silently recovered and resynced. This replaces
the beforeunload "leave site?" warning, which only blocked navigation - it
never actually saved anything.

A toast (with a Retry action) surfaces a save failure, since there's no more
persistent status indicator to show it.

* improvement(files): simplify autosave draft persistence and dedupe editor resync

Structural cleanup after the local-draft feature: draftKey is now ANDed with
enabled inside useAutosave itself (rather than trusting every caller to
replicate that gating), the combined dirty-transition/debounce effect is
split into two single-purpose effects, redundant back-to-back IndexedDB
writes on visibilitychange+pagehide are deduped, a dead identity wrapper
around setDraftContent is removed, and the three near-identical
"resync editor body if changed" blocks in rich-markdown-editor.tsx collapse
into one local helper.

* fix(files): flush pending local draft on unmount, fix stale Retry target

Two real bugs from Greptile's first review pass:
- Unmounting before the 400ms local-draft debounce fired cancelled the
  pending timer without ever writing the draft, so if the network flush
  also failed on the way out, the edit had no backup anywhere. The unmount
  cleanup now calls persistLocalDraft() synchronously before attempting
  the network flush.
- The save-failure toast's Retry action read saveRef.current lazily at
  click time, so navigating to a different file before clicking Retry
  would retry-save the wrong file. It now captures the failing file's
  save function at the moment the toast is created.

* fix(files): Discard Changes now actually resets editor content

Discard previously only cleared the parent's mirrored isDirty/saveStatus
state; the editor's own content was never reset to match the server
baseline. On unmount, useAutosave's flush logic saw content still
diverged and (a) re-saved the "discarded" edit to the server, and (b)
after this PR's local-draft addition, also persisted it to IndexedDB —
so even a future fix to (a) would still have the draft resurrect the
discarded text on next open.

Adds a discardRef bridge (mirrors the existing saveRef pattern) so
Discard resets the editor's draft content back to savedContent before
navigating away, closing both paths at the root.

* fix(files): make Discard deterministic, independent of state-update timing

The previous discard fix (setDraftContent(savedContent) before navigating)
relied on that dispatch landing before the FileViewer unmounts. If unmount
raced ahead of it, the autosave cleanup would still see stale dirty content
and could resurrect the discarded edit via the local draft.

useAutosave now exposes discard(): it flags the instance as discarded,
cancels any pending timers, and clears the local draft immediately. Every
write path (persistLocalDraft, save, the unmount flush) checks that flag
first, so nothing written after discard() can bring the edit back,
regardless of whether the content-reset render has committed yet.

* fix(files): correct in-flight save after discard, fix IndexedDB write/delete ordering

Two more real races from round 4 of review:
- discard() couldn't stop a save that had already started (discardedRef
  only blocks saves not yet begun). Once that in-flight save lands, it
  now schedules a corrective save to push the reverted content, rather
  than leaving the discarded edit on the server permanently. Only fires
  when a save was genuinely in flight at discard time.
- persistLocalDraft's set() and clearLocalDraft's del() were independent
  promises with no ordering guarantee. A slow write starting before
  discard could resolve after discard's delete and resurrect the draft.
  Both now go through a single serialized queue per hook instance, so a
  delete queued after a write always runs after it completes.

* fix(files): make discard's corrective save use an explicit baseline

The corrective save (from the previous fix) relied on the caller's
setDraftContent(savedContent) having landed by the time it ran — a real
race, not a guarantee: React commits that render on its own schedule,
and if the correction's continuation runs first, onSave still reads the
ambient (still-dirty) content ref and re-persists the discarded edit.

onSave now accepts an optional override content; discard() captures
savedContentRef.current as an explicit target at the moment it's called
and passes it through, so the correction always pushes the true reverted
baseline regardless of render timing. Widened the shared onSave type is
backward compatible — the other useAutosave caller (chunk-editor) ignores
the extra optional param.

* fix(files): retry no longer depends on a remount-able shared ref, purge stale drafts

Two more from round 6:
- The failure toast's Retry action captured saveRef.current inside the
  effect reacting to saveStatus='error' — but if the user switched files
  between the failure occurring and that effect committing, the keyed
  remount could have already repointed saveRef at the new file's save
  function first. onSaveStatusChange now passes the failing instance's
  own saveImmediately alongside the 'error' status directly from the
  hook that owns it, so retry can never be sourced from the wrong file
  regardless of remount timing.
- A local draft with a stale (mismatched) baseline was left in IndexedDB
  after being correctly skipped for recovery, so it could resurrect later
  if the server baseline ever coincidentally matched it again. It's now
  purged as soon as it's identified as stale.

* fix(files): clear inFlightRef once a save settles

inFlightRef.current was never reset after a save resolved or rejected —
it stayed pointing at the (now-fulfilled) promise indefinitely. discard()
reads it to decide whether a save is genuinely in flight; since a
resolved promise is still truthy, discard() treated any prior completed
save as still pending, captured savedContentRef.current as the
"corrective" target, and could push a stale baseline if that capture
happened before the save's own dispatch had updated it.

Now cleared to null as soon as the save settles, so discard()'s
in-flight check reflects reality regardless of how long ago the last
save finished.

* fix(files): surface a failed discard correction, resume autosave after discard if editing continues

Two more from round 8:
- If discard()'s corrective save failed, it was only logged — the server
  could permanently keep the discarded edit with zero user-facing signal.
  Now surfaced via a dedicated onDiscardCorrectionFailed callback, which
  closes over the specific file's own name rather than routing through
  the shared onSaveStatusChange path (that path reads whichever file is
  currently selected, which by the time this fires is already the file
  the user navigated to, not the discarded one).
- discardedRef never cleared once set, so if the editor stayed mounted
  briefly after discard (before navigation completes) and the user typed
  again, every save path silently no-op'd forever for that new edit too.
  It now clears itself as soon as a genuinely new edit (content diverging
  from savedContent again) is observed.

* fix(files): serialize local drafts by key across mounts, not just within one

idbQueueRef was a per-instance ref, so it only ordered IndexedDB ops issued
by the same hook instance. A slow write queued by an unmount's flush lived
on as a bare promise after that instance was gone, with nothing sequencing
it against a freshly-mounted instance for the same file — its del() or
recovery read could run first, and the late write would land afterward and
resurrect a draft that was supposed to be gone.

Replaced the per-instance ref with a module-level queue keyed by draft key,
shared by every useAutosave instance (past or present) touching that key,
so ordering holds across a fast unmount+remount of the same file.

* improvement(files): consolidate autosave hook after 9 rounds of incremental fixes

Cosmetic-only pass, no behavior change:
- Hoisted MIN_SAVING_DISPLAY_MS to module scope alongside LOCAL_DRAFT_DELAY_MS
  (was declared inside the hook body, re-allocated every render, inconsistent
  with its sibling constant).
- Grouped the ~15 refs by concern (save/network, content mirrors, draft-key +
  callbacks, local-draft persistence, discard) instead of the chronological
  order they were added across nine review rounds.
- Removed a provably-dead re-check in the unmount cleanup: content/savedContent
  can't change between the outer guard and the inner one (no renders happen
  post-unmount), so only the discardedRef half of the inner check was live.
- Removed an unnecessary useCallback around onDiscardCorrectionFailed — its
  reference is never observed by anything (useAutosave copies it into a ref
  every render regardless of identity), unlike handleSaveStatusChange in
  files.tsx, which is correctly memoized because it flows through
  React.memo-wrapped TextEditor/RichMarkdownEditor.

Validated against external research: the two-tier debounce (network + local
IndexedDB draft) matches how Tiptap/Notion describe their own local-first
persistence; the discardedRef+corrective-save approach over AbortController
is a deliberate, justified choice (onSave has no signal parameter, and an
abort can't undo a write that's already landed server-side); the module-level
per-key promise queue is a recognized idiomatic pattern. Splitting this hook
into three smaller ones (useDebouncedSave/useLocalDraft/useDiscard) is a
legitimate future refactor, deliberately deferred given the risk of touching
this heavily-interdependent, already-hardened state this late in review.

* fix(files): serialize discard correction against newer saves, recover local drafts only once per mount

Two more real races, both interactions between earlier fixes:
- discard()'s corrective save and a genuinely new edit made right after
  could race independently: if the user typed again before the correction
  fired, and that correction landed after the new edit's own save, the
  server would end up with the discarded baseline instead of the user's
  latest content. The correction now shares the same inFlightRef/savingRef
  mutual exclusion normal saves use, and skips entirely once content has
  moved on to something that's neither the discarded baseline nor what it
  was at the moment discard() was called.
- The local-draft recovery effect re-ran every time draftKey toggled
  through enabled (e.g. autosave turning off during agent streaming and
  back on once it settles), re-scanning IndexedDB as if freshly mounted.
  If the settled content coincidentally matched the stale draft's stored
  baseline, a pre-stream local edit could silently overwrite the agent's
  work. Recovery now attempts exactly once per mount.
2026-07-09 18:09:41 -07:00
Waleed cbe5e0f3f4 fix(uploads): fix Azure Blob connection-string-only auth and document self-host parity (#5553)
* fix(uploads): fix Azure Blob connection-string-only auth and document self-host parity

Every Azure Blob operation (upload/download/delete/head/presigned URLs) threw
when only AZURE_CONNECTION_STRING was set, despite that being the documented
alternative to AZURE_ACCOUNT_NAME/KEY across .env.example, env.ts, and the
Helm chart. createBlobConfig required accountName unconditionally, and the
upload-SAS path had no fallback to derive credentials from the connection
string. Fixed both, verified end-to-end against a live Azurite emulator
(upload, download, head, delete, multipart/block-blob upload, and real
HTTP PUT/GET through generated SAS URLs), and added regression tests.

Also closes the remaining self-host Azure documentation gaps: AZURE_ACS_CONNECTION_STRING,
OCR_AZURE_*, KB_OPENAI_MODEL_NAME, and WAND_OPENAI_MODEL_NAME are now documented in
the Helm chart (values.yaml, values.schema.json, values-azure.yaml) and
.env.example alongside their AWS/S3 counterparts.

* fix(uploads): map new Azure keys in the ESO remoteRefs example

Greptile flagged that the values-azure.yaml External Secrets example
didn't map KB_OPENAI_MODEL_NAME, WAND_OPENAI_MODEL_NAME, OCR_AZURE_ENDPOINT,
and OCR_AZURE_MODEL_NAME, so a user who fills those in and switches to ESO
would hit a Helm template render failure. Added the remoteRefs entries and
verified with an isolated helm template render.
2026-07-09 18:09:27 -07:00
Theodore Li cc7a6a82f8 fix(custom-blocks): stop draft-load sanitization deleting consumer-typed input values (#5551) 2026-07-09 19:47:21 -04: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 aa5b1d5569 fix(suggested-actions): swap unaudited filled icons for EMCN outline set (#5548)
* fix(suggested-actions): swap unaudited filled icons for EMCN outline set

Suggested-action template icons on the home page mixed filled/solid
icons in with the app's outline icon convention. Swapped them for
consistent outline icons and removed the unused filled Card icon.

- gmail.ts: Card -> ClipboardList
- clickhouse.ts, sftp.ts: Trash -> TrashOutline
- ssh.ts: TerminalWindow (emcn) -> SshTerminalIcon (moved to
  components/icons.tsx alongside the other block/brand icons)
- deleted unused packages/emcn/src/icons/card.tsx
- regenerated docs

* fix(docs): revert jira.mdx regen regression

generate-docs.ts is dropping the Configuration and generic-webhook
Output tables for Jira triggers even though the trigger schemas still
define those fields (caught by Greptile review). Unrelated to the
icon changes in this PR, so reverting jira.mdx to its prior content
rather than debugging the generator here.

* fix(build): remove last Card icon consumer in playground gallery

apps/sim/app/playground/page.tsx imported Card from the top-level
@sim/emcn barrel for the icon showcase grid, which I missed when
auditing @sim/emcn/icons consumers. Broke the production build after
card.tsx was deleted. Removed the import and its gallery entry.
Verified with a local `bun run build`.
2026-07-09 15:55:58 -07:00
Waleed ce32993c76 fix(knowledge): fix chunk_index race and storage-quota check/increment race (#5544)
* fix(knowledge): fix chunk_index race and storage-quota check/increment race

- createChunk now serializes concurrent writes to the same document via a
  transactional advisory lock before computing the next chunk_index, fixing
  the unique-constraint collision behind the ITSM 'Failed to create chunk'
  incident (two overlapping workflow runs appending to the same shared KB
  document).
- Document uploads now check and increment storage quota atomically in a
  single conditional UPDATE inside the insert transaction, instead of
  check-then-insert-then-increment-after-commit, closing the window where
  two concurrent uploads could both pass the quota check and over-commit
  storage.

* fix(knowledge): bound the chunk advisory-lock wait with lock_timeout

Address Greptile P1: the advisory lock in createChunk could wait
indefinitely on a stalled same-document transaction while holding a pooled
connection. Set a 5s lock_timeout before acquiring it, matching the
set_config + pg_advisory_xact_lock pattern already used by every other
advisory lock in this codebase (org membership, table schema/row-ordering,
BYOK keys, workspace env, usage-log flush, execution-log reconciliation).
2026-07-09 14:54:46 -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 6168cff79a fix(icons): fix Meta icon clipping in model dropdown (#5542)
MetaIcon's viewBox (265x165) was smaller than the true bounding box of
its three path elements (287.56x191, computed from the actual path
geometry), so the bottom-right of the mark was silently clipped by the
SVG viewport. It was also non-square, so the dropdown row's forced
14x14 icon slot letterboxed it asymmetrically instead of filling it
like every other provider icon.

Fixed the viewBox to the exact computed bounding box, padded to a
square and vertically centered (0 -48.28 287.56 287.56) - matches the
sibling icon convention (GeminiIcon, etc.) of a square viewBox filling
the icon slot edge to edge. Verified by rasterizing both the old and
new viewBox at the actual deployed 14px size.
2026-07-09 13:57:18 -07:00
Waleed e802da1075 feat(rich-markdown-editor): round-trip gate, VSCode/style paste, highlight, block reorder (#5539)
* fix(rich-markdown-editor): tighten read-only gate for uppercase entities and orphan ref-defs

Two silent-corruption cases the idempotency probe can't see:

- The HTML-entity safe-list used a case-insensitive regex, so `&AMP;`/`&LT;`/`&GT;` were
  treated as the round-trippable canonical entities and let through as editable, but the
  serializer only round-trips the lowercase forms and mangles the uppercase ones. Make the
  safe-list case-sensitive.
- An unused link/image reference definition (`[x]: url` with no `[x]` reference) is dropped
  entirely on serialize, a deletion the idempotency probe misses. Detect orphan definitions
  and open read-only; used definitions still inline losslessly and stay editable. The use
  check tolerates bracket-internal padding (`[ x ]`), and GFM footnote definitions (`[^id]: …`)
  are excluded since they round-trip verbatim.

* feat(rich-markdown-editor): VSCode code paste and strip <style>/<script> from pasted HTML

- Code copied from VSCode carries a `vscode-editor-data` payload with the source language,
  but its text/html is per-token colored spans that ProseMirror flattens into plain
  paragraphs. Read the payload and paste a real fenced code block with the mapped language.
  markdown/plaintext modes resolve to no language and fall through, so markdown copied from
  VSCode still parses as rich content rather than a fenced block.
- Google Sheets and Word prepend a `<style>` block of CSS that PM's DOM parser walks into
  the document as literal text. Strip <style>/<script> in transformPastedHTML before parsing.
- Add a `==…==` inline-mark hint (allowing a lone interior `=`) so a plain-text paste of
  `==highlight==` routes through the markdown parser and becomes a highlight mark.

* feat(rich-markdown-editor): add highlight (==mark==) support

Adds a highlight mark rendered as <mark> and serialized to/from ==text== (Pandoc/Obsidian
syntax). A custom inline tokenizer parses ==text== (inner text parsed as inline markdown so
nested marks like ==**bold**== survive; the body allows a lone `=` so ==a=b== round-trips).
`==` cannot be encoded in the delimiter, so an appendTransaction guard strips the mark from
any text that ends up containing `==` (e.g. a toolbar highlight over a==b), keeping the text
and never emitting the corrupting ==a==b==. Comparison operators (x == y) stay literal.
Wired with an input rule, paste rule, Mod-Shift-H, a bubble-menu button, and themed <mark>
styling. Also locks in mark-stacking round-trips across contexts.

* feat(rich-markdown-editor): keyboard block reordering (Mod-Shift-Arrow)

Mod-Shift-ArrowUp/ArrowDown swaps the current top-level block with its neighbour, carrying
the caret at its original offset (newBefore + offset, no off-by-one), and no-ops at the
document edges. Exposed as moveBlockUp/moveBlockDown commands (the keyboard shortcuts call
them). Pure UI interaction, no schema change. Covered by tests (order, caret offset, edge
no-op, list stays intact).
2026-07-09 13:31:16 -07:00
Waleed 29c01fe40a feat(providers): add Meta Muse Spark 1.1 provider (#5538)
* feat(providers): add Meta Muse Spark 1.1 provider

- New BYOK-only meta provider for Meta's Model API (launched today)
- muse-spark-1.1: 1M context, streaming, tool-calling, reasoning_effort
  (minimal->xhigh), structured output
- Meta icon mark only (no wordmark), theme-safe gradient IDs via useId()
- Not added to hosted models list - BYOK only, no auto-billing

* fix(providers): stop sending unsupported tool_choice values to Meta

Meta's Chat Completions endpoint only supports tool_choice: "auto" -
"none", "required", and named-function choices all return HTTP 400
(confirmed against the official meta-model-cookbook tool-calling
recipe). Never set tool_choice on the request (auto is already the
default; forced-tool usage control degrades gracefully to auto with a
warning log instead of failing every tool-using run), and drop `tools`
entirely from the two post-tool-loop tool-free completion calls
instead of trying to force tool_choice: "none".
2026-07-09 11:40:11 -07:00
Waleed b117758eaa fix(agent): scope nested tool canonical-mode overrides by instance, not type (#5534)
Two tool entries of the same type inside an Agent block's tool-input array
(e.g. two Table tools) shared a single canonical-mode override keyed by
${toolType}:${canonicalId}, so switching basic/advanced mode on one field
silently switched it on every other instance of the same tool type -
including at execution time, where the wrong basic/advanced value could be
resolved for the second tool.

Rescope the override key to the tool's position in its tool-input array
(${toolIndex}:${canonicalId}) instead of its type, and thread that index
through every consumer: the editor (read + write), execution
(agent-handler/providers), search-index, and fork/promote remapping.
2026-07-09 11:21:05 -07:00
Waleed b09e0c0d3b feat(providers): add OpenAI GPT-5.6 Sol, Terra, and Luna models (#5537) 2026-07-09 11:05:46 -07:00
Waleed 5db62b8bdc feat(observability): extend audit log, PostHog, and storage metering coverage (#5269)
* feat(observability): extend audit log, PostHog, and storage metering coverage

Instruments previously-uncaptured resources and actions across the three
observability layers (audit log, PostHog product analytics, usage metering):

- Exfiltration audit: file downloads (workspace/public-share/v1), table,
  workflow, and workspace exports.
- Credential access audited at the token-issuance boundary (success-only).
- Full revenue trail: invoice paid/failed, overage billed, disputes, credit
  fulfillment, subscription lifecycle, plan/seat changes.
- Session/login lifecycle audit via Better Auth hooks (login, blocked sign-in,
  logout, session revoke, account delete).
- v1/admin programmatic surface and copilot tool handlers instrumented.
- Dead audit/PostHog constants wired (lock/unlock, table, custom tool, etc.).
- Storage metering extended to KB documents and copilot files.
- PostHog person identify + workspace/organization group hygiene.

Audit package hardened to null the actor FK for system actors (admin-api) and
adds an awaitable recordAuditNow for pre-delete hooks. All instrumentation is
fire-and-forget and never blocks or breaks the primary operation.

* fix(observability): audit file/credential egress only on success

Address Cursor Bugbot review:
- GET OAuth token: emit CREDENTIAL_ACCESSED/credential_used after
  refreshTokenIfNeeded succeeds (matches POST), not before.
- File export: audit on each actual success exit via a shared helper —
  including the non-markdown serve redirect (previously unaudited) and
  after the zip is generated (previously before asset fetch).

* fix(audit): record null actor for anonymous public-share downloads

Address Greptile P1: setting actorId to the file owner made every anonymous
external download read as a self-download, undermining the exfiltration trail.
recordAudit now accepts a null actor; the public content/inline routes record
actorId=null with the owner in metadata.sharedByUserId and rely on ip/user-agent
for the forensic trail. The misleading owner-attributed file_downloaded PostHog
event is dropped on these anonymous paths.

* fix(observability): audit admin exports after zip; tidy comments

- Admin workflow/workspace ZIP exports now audit only after the archive is
  built (via a local helper), so a zip/build failure no longer logs an export.
- Remove redundant 'success-only placement' inline comments and tighten the
  remaining design-rationale notes (export helper now TSDoc).

* fix(billing): complete the chargeback + overage financial trail

Address Cursor review:
- handleDisputeClosed now records CHARGE_DISPUTE_CLOSED for every closed
  dispute (won/lost/warning_closed), unblocking only on favorable outcomes.
  dispute.status in the metadata distinguishes the outcome, so lost
  chargebacks are no longer missing from the trail.
- Threshold overage now emits OVERAGE_BILLED + overage_billed even when credits
  fully cover the overage (settledVia: 'credits' vs 'stripe'), so credit-settled
  overages are audited instead of silently returning null.

* fix(storage): decrement copilot quota before deleting metadata

Address Greptile P1: deleting the metadata row before the decrement meant a
decrement failure left the quota permanently inflated with no record to retry
from. Decrement first; only remove the metadata row once it succeeds.

* fix(observability): atomic copilot storage release; signup-blocked action

- Copilot file delete now releases storage via a single transaction
  (releaseDeletedFileStorage): the soft-delete is the idempotency claim and the
  decrement shares the transaction, so neither a partial failure (inflated
  counter) nor a retry (double-decrement) can desync the quota. Resolves the
  conflicting Greptile/Cursor ordering findings.
- Policy-blocked sign-ups now record USER_SIGNUP_BLOCKED instead of
  USER_SIGNIN_BLOCKED, so account-lifecycle events aren't mislabeled.

* fix(storage): meter copilot ingest centrally for path symmetry

Address Cursor review: only uploadCopilotFile incremented storage, but copilot
files also enter via the generic upload route and presigned uploads — all of
which persist metadata through insertFileMetadata. Move the increment into
insertFileMetadata (scoped to context='copilot', on genuine insert/restore) so
every ingest path is symmetric with the delete-time decrement, and drop the now
redundant per-path increment in uploadCopilotFile. Other contexts are metered by
their own managers and remain unaffected.

* fix(audit): skip WORKFLOW_EXPORTED when admin export is empty

Address Cursor review: when every requested workflow fails to load, the admin
export still returned 200 but recorded a successful export of zero workflows.
Guard auditExport so an empty result records nothing.

* fix(storage): settle copilot accounting before deleting the blob

Address Cursor review: the blob was removed before releaseDeletedFileStorage, so
a release failure left the counter inflated and the metadata active with the blob
gone. Now the atomic soft-delete + decrement runs first and the blob is deleted
only if it succeeds, so a failure leaves the file fully intact and retryable.

* fix(storage): decrement KB document storage atomically with deletion

Address Cursor review: hardDeleteDocuments deleted the rows then decremented
best-effort, so a decrement failure left billed storage inflated with no row to
reconcile. Resolve each owner's subscription up front, then decrement inside the
same transaction that deletes the embeddings/documents (decrementStorageUsageInTx,
also now shared by releaseDeletedFileStorage), so the counter and the content
commit or roll back together. Connector docs remain excluded.

* fix(audit): don't treat email verification as a login

Address Cursor review: /verify-email could emit USER_LOGIN when verification
mints or refreshes a session, mislabeling (or double-counting) a non-sign-in as
a login. Restrict isLoginPath to genuine sign-in entrypoints.

* fix(audit): null actor FK when the user lookup throws

Address Greptile P1: the catch branch left the original actorId, so a system
actor like 'admin-api' (or a since-deleted user) would FK-violate the insert and
lose the row when the existence lookup errored. Mirror the not-found branch —
null the FK with a readable label — so the audit row always persists.

* revert(audit): drop session/account-lifecycle auth instrumentation

Remove the Better Auth login/logout/session-revoke/account-delete/blocked-signin
audit hooks from auth.ts — they touch sensitive auth paths and are noisy. Also
removes the now-unused taxonomy (USER_LOGIN/_FAILED, USER_SIGNIN_BLOCKED,
USER_SIGNUP_BLOCKED, USER_LOGOUT, SESSION_REVOKED, ACCOUNT_DELETED,
ACCOUNT_EMAIL_CHANGED actions; SESSION/USER resource types) and the awaitable
recordAuditNow helper that only the pre-delete hook used. auth.ts is back to the
staging baseline.

* fix(storage): harden copilot+KB delete accounting against read errors and concurrency

Final-audit follow-ups:
- deleteCopilotFile: a failed metadata *read* (vs a genuine missing row) now
  blocks the blob delete too, so a transient read error can't leave an active
  row un-decremented with the blob gone.
- hardDeleteDocuments: drive the per-user decrement from the delete's
  returning() (the rows this tx actually removed), so two concurrent deletes of
  the same ids can't both decrement.

* chore(observability): drop two unused definitions

Final-audit cleanup: remove the orphaned knowledge_base_searched PostHog event
(never wired; KB search analytics already flow through the OpenTelemetry channel)
and the redundant AuditAction.SUBSCRIPTION_UPDATED (subscription/plan changes are
audited via ORG_PLAN_CONVERTED). Every remaining new action/event has a real
emit site.

* fix(knowledge): key hard-delete result off rows actually deleted

Address Cursor review: hardDeleteDocuments returned existingIds.length (the
requested count) and cleaned storage for the full pre-tx set, even though the
decrement was driven by the rows the transaction actually deleted. Under a
concurrent delete that claimed some ids first, that overstated the result and
re-touched storage for rows this call didn't delete. Drive the storage cleanup,
log, and return value from deletedDocs (the returning() rows) so all four are
consistent.

* fix(storage): gate copilot quota on all ingest paths; tidy inline comments

- Copilot uploads via the generic /api/files/upload route and presigned URLs now
  run checkStorageQuota before writing (matching uploadCopilotFile), so no copilot
  ingest path can grow usage past the plan limit. The central increment stays in
  insertFileMetadata.
- Trim/remove redundant inline comments across the diff; keep only concise notes
  on non-obvious decisions (left pre-existing comments untouched).

* fix(analytics): omit empty workspace_id on workspace-less file downloads

Address Greptile P1: the generic key-based /api/files/download route and the
no-workspace markdown-export path emitted file_downloaded with workspace_id:''
creating a phantom '' bucket in PostHog. Make workspace_id optional on the event
and omit it when there is no workspace (workspace-scoped routes still pass it).

* fix(analytics): omit empty workspace_id/workflow_id on copilot_chat_sent

Final-sweep nit: copilot_chat_sent sent '' for workspace_id/workflow_id in the
agent (workspace-less) branch, same phantom-bucket issue as file_downloaded.
Make both properties optional and omit them when absent.

* fix(analytics): clear stale org PostHog group on personal workspaces

Address Cursor review: switching from a team workspace to a personal one left
the previous organization group set, so later events kept rolling up under it.
When the active workspace has no organizationId, resetGroups() to drop the stale
org group, then re-apply the workspace group.

* fix: address review — export audit timing, copilot delete signal, group reset

- Table export (Cursor MED): audit fires before streaming begins, not after
  controller.close(), so a mid-stream failure still records the partial export.
- deleteCopilotFile (Greptile P1): throw when storage accounting can't be settled
  instead of silently returning, so callers can detect the file was not deleted.
- workspace-scope-sync (Cursor MED): only resetGroups() once workspace metadata is
  loaded (activeWorkspace present), so a team workspace doesn't transiently lose
  its org group while organizationId is still null during load.

* refactor(observability): final line-justification cleanup

Address final audit flags:
- Table import: move the 'columns added' audit OUT of the import transaction
  into a post-commit auditTableColumnsAdded() helper called by the three
  tx-owning callers, so a mid-import row-batch rollback no longer logs a false
  'added N columns' (matches the PR's success-only discipline).
- Omit empty-string analytics dimensions on workflow_lock_toggled (workspace_id)
  and organization_created (name), consistent with file_downloaded/copilot_chat_sent.
- Restore an unrelated capacity-check comment removed incidentally.
- Move copilot_chat_sent emit above the traceparent comment so the comment sits
  with the code it documents.

* fix(table): attribute import column audit to the importing user

Address Cursor review: addImportColumns (async createColumns import path) now
threads the importing userId into auditTableColumnsAdded instead of falling back
to table.createdBy, so column additions are attributed to the actual member who
ran the import rather than the table creator.

* feat(observability): drop copilot from storage accounting

Per design decision: copilot files are working/conversational artifacts, so
gating their materialization on storage quota would fail an agent operation
mid-flow when a user is over limit, and metering them would inflate usage enough
to block KB/workspace uploads indirectly. Remove copilot quota gates + copilot
ingest metering entirely (revert copilot-file-manager, metadata, and the upload
route's copilot branch to baseline) and drop the now-unused releaseDeletedFileStorage.
KB document metering + quota enforcement (the deliberate, persistent storage path)
is unchanged.

* fix(analytics): switch workspace + org PostHog groups together

Address Cursor review: gate the group-sync effect on workspace metadata being
loaded (activeWorkspace present) so the workspace and organization groups always
update atomically. Acting during the load window paired the new workspace group
with the previous workspace's org group; until metadata loads, events stay
consistently attributed to the previous workspace.

* fix(table): audit async export at authorization, not job completion

Address Cursor review: async exports only emitted TABLE_EXPORTED when the
background job reached 'ready', so an authorized export whose job later failed or
was abandoned left no audit trail — inconsistent with the sync export route,
which audits before streaming. Move the audit + analytics to the async route's
authorization point (after the job is claimed/dispatched) and remove it from the
runner. Drop the now-unused userId from TableExportPayload.

* fix(billing): never let payment_failed instrumentation skip user blocking

Address Greptile P1: the payment_failed audit hoisted an unguarded
isSubscriptionOrgScoped DB read (for entity_type) directly before the
attempt-count user-blocking block. A transient failure of that read would throw
out of the handler and skip blocking. Wrap the whole audit/analytics block in
try/catch (best-effort), and let the blocking compute its own isSubscriptionOrgScoped
as it did originally — instrumentation can no longer abort payment processing.

* chore(observability): trim verbose inline comments to concise notes

* fix(billing): wrap new dispute/enterprise/subscription instrumentation in Stripe webhook idempotency

recordAudit/captureServerEvent calls added for charge disputes, enterprise
subscription provisioning, and free->paid subscription creation ran
unconditionally with no idempotency guard, unlike their sibling handlers
in the same files. Stripe redelivers webhooks at-least-once, so a retry
would double-record the audit row and PostHog event even though the
underlying DB writes were already idempotent.

* fix(observability): tag org-scoped audit metadata with organizationId, guard concurrent table delete

The org-scoped self-service audit-log endpoint matches org-level rows via
metadata.organizationId (or resourceType=organization). Six recordAudit
call sites (subscription create/cancel, admin credit issuance, threshold
overage billing, charge disputes, credit purchase, invoice payment
succeeded/failed) tagged org-scoped events with a differently-named key
(referenceId/entityId/targetOrgId), making them invisible to org admins
querying their own audit trail despite being stored in the DB. Add the
missing organizationId key everywhere the pattern was missed.

Also guard deleteTable's archive UPDATE with isNull(archivedAt) so a
concurrent duplicate delete request is a no-op instead of re-archiving
and re-firing a duplicate TABLE_DELETED audit row.

Adds test coverage for the ORG_MEMBER_ADDED audit/analytics emission in
acceptInvitation, which previously had none.

* fix(test): queue resolveBillingActorId's owner lookup in payment-failure email test

handleInvoicePaymentFailed's new payment_failed audit instrumentation
resolves the billing actor via an extra db.select before
sendPaymentFailureEmails runs. The test's fixed select-response queue
didn't account for it, so the org-admin lookup consumed the wrong
queued row and the assertion saw zero email sends. Production behavior
is unaffected — each query is independent; this was a mock-queue
ordering issue only.

* fix(billing): don't let a post-commit usage-limit sync failure suppress the seat audit

Cursor Bugbot flagged: reconcileOrganizationSeats committed the seat
change and Stripe outbox enqueue in a transaction, then called
syncSubscriptionUsageLimits outside it before recording the audit/
PostHog events. A thrown sync left a genuinely-changed seat count with
no ORG_SEAT_PROVISIONED/DEPROVISIONED trail. Wrap the sync in its own
try/catch (log + continue) so the events always fire for a committed
change, matching the fire-and-forget instrumentation pattern used
elsewhere in this PR.

* fix(billing): guard the new subscription-created instrumentation block

Greptile P1: the org-scope check I added for the audit-metadata fix
(isSubscriptionOrgScoped) was a raw DB call with no error guard, unlike
resolveSubscriptionActorId next to it. Since it ran inside the
idempotency lambda with an unconditional rethrow, a transient DB error
would abort and retry the whole webhook after the free -> paid usage
reset had already committed. Wrap the actor/org-scope resolution +
audit + analytics block in try/catch, matching the same guarded
instrumentation pattern already used in handleInvoicePaymentFailed.
2026-07-08 23:23:59 -07:00
Vikhyath Mondreti fea3391015 improvement(forking): minor ui improvements (#5533)
* improvement(forking): minor ui improvements

* fix warning
2026-07-08 22:53:52 -07:00
Waleed 472457b082 chore(skill): check branch is synced with origin/staging before pushing (#5529)
* chore(ship-skill): check branch is synced with origin/staging before pushing

Prevents PRs from silently picking up extraneous commits when a
branch/worktree is cut from a stale or diverged local staging/main.

* feat(babysit): add skill to drive a PR through review to a clean 5/5

Ships (via /ship's sync check), waits for Greptile/Cursor Bugbot,
triages every open thread (fix real findings, push back on false
positives), replies + resolves each thread, re-triggers review, and
loops until Greptile is 5/5 with zero open threads.

* chore(cursor-commands): mirror sync-check into ship.md, add babysit.md

Keeps the Cursor /commands mirrors of ship and babysit in sync with
the Claude Code .agents/skills versions so they don't drift.

* fix(ship-skill): compare commit content not count, handle WIP before rebase, avoid fixed temp branch name

- Final verify now diffs actual commit subjects between git log and
  the PR, not just a count — a corrupted branch's inflated count could
  coincidentally match a later count check
- The pre-commit count in step 2 was being compared against the
  post-commit PR in step 9, which would mismatch even on success; now
  compares content at the point it's actually available
- Recovery flow stashes uncommitted work before rebasing so it isn't
  blocked by a dirty tree, and checks for an existing tmp branch
  before reusing the name

* fix(ship-skill,babysit): clean rebase can still hide drift, fixed temp-branch name blocks recovery

- A clean git rebase can replay stray commits with zero conflicts, so
  'rebase succeeded' was being treated as sufficient — now the log is
  always re-checked after rebasing, clean or not, before trusting it
- The recovery text told the agent to 'pick another name' if
  ship-sync-tmp was taken but then hardcoded that literal name anyway;
  now it just deletes the leftover (disposable, single-purpose) branch
  instead of introducing a naming scheme
- babysit's step 5 pointed at re-running the log check alone; it now
  points at the full /ship step 2 recovery flow, since a review loop
  spanning a long session is exactly where drift compounds silently

* fix(ship-skill,babysit): guard tmp-branch delete, fetch before final verify, paginate threads, verify after every push

- ship-sync-tmp deletion is now conditional on the branch existing —
  a bare 'git branch -D' on a first recovery attempt with no leftover
  would fail and block the rebuild before it started
- step 9's final commit-content verify now fetches origin/staging
  first, matching step 2; a stale local ref could mask real drift
- babysit's reviewThreads query now checks pageInfo.hasNextPage and
  pages through all threads instead of assuming 50 is always enough
- babysit now re-runs the sync-content verify after every push, not
  just before it, so a bad push or a PR that drifted mid-loop doesn't
  go unnoticed across review rounds

* fix(ship-skill,babysit): fix reversed commit-list order, tail-1 grabbing wrong line, stale step reference

- git log --oneline is newest-first, gh pr view commits is oldest-first
  — the two content-verify commands now use --reverse so a positional
  comparison doesn't spuriously fail on any multi-commit branch
- 'select(...) | .body | tail -1' pipes every matching comment's full
  body through the pipeline and keeps only the last LINE of the
  combined output (the review-count footer), not the last COMMENT —
  demonstrated this myself this session reading the footer instead of
  the actual score. Now uses '[.comments[]] | last | .body'
- Cursor's babysit.md pointed at '/ship step 9', but Cursor's ship.md
  only has 7 steps (no cleanup/migration steps) — now points at step 7
  and cross-references the 9-step Claude Code skill copy

* fix(ship-skill): checkout before deleting ship-sync-tmp, cherry-pick oldest-first

- git refuses to delete the branch currently checked out, so if an
  earlier interrupted recovery left the agent sitting on ship-sync-tmp,
  the conditional delete would find the branch and then fail to remove
  it, blocking the rest of the rebuild. Now checks out the original
  branch first (a no-op if already there).
- git log's default order is newest-first; cherry-picking session SHAs
  in that displayed order applies the newest commit before older ones,
  which can fail or produce the wrong history with 2+ session commits.
  Now explicit: cherry-pick oldest-first, using --reverse to get them
  in that order directly.

* fix(ship-skill): capture cherry-pick SHAs before switching to the temp branch, make tmp delete idempotent

- Step 4 of the rebuild checks out ship-sync-tmp at origin/staging, so
  HEAD no longer contains the session's commits by the time the old
  step 4 (now step 5) ran 'origin/staging..HEAD' to find them — that
  range was always empty by then, following the steps literally would
  cherry-pick nothing. Now captures the SHA list first, against the
  original branch name explicitly, before any branch switch.
- The show-ref-guarded delete still exits 1 (git show-ref's own
  failure) on the common first-attempt case where there's no leftover
  branch, which could halt a caller that stops on nonzero exit before
  ever reaching the rebuild. Replaced with a plain delete + || true,
  which always succeeds whether or not there was anything to delete.

* fix(ship-skill,babysit): rebuild was replaying the exact stray commits it was meant to leave behind

- The 'capture the range' step from the previous round's fix captured
  origin/staging..<original-branch> wholesale — but in exactly the
  scenario that triggers this rebuild, that range also contains the
  unrecognized/stray commits, so cherry-picking 'all of it' recreated
  the same polluted branch. Now explicit: read the log, write down
  only the SHA(s) you recognize as your own, never the whole range.
- babysit's pre-push gate list named lint/typecheck/boundary-validation
  but dropped ship's conditional /cleanup and /db-migrate steps, which
  a review-fix round can trip just as easily as the original commit.
- babysit's force-with-lease condition only covered the cherry-pick
  rebuild path, but a plain 'git rebase origin/staging' that completes
  with no conflicts also rewrites already-published history and needs
  the same force push.

* fix(ship-skill): push step needs force-with-lease too, not just babysit's

Step 2's sync recovery rewrites already-pushed commits on a branch
that's been pushed before, but the push step only said 'push using
the current branch name' with no force-with-lease — a plain push
would be rejected in exactly the polluted-remote case step 2 exists
to fix. babysit already got this fix; ship's own push step didn't.
2026-07-08 19:46:14 -07:00
Waleed 6e87d74a93 feat(jupyter): add Jupyter integration (contents, kernels, sessions) (#5527)
* feat(jupyter): add Jupyter integration (contents, kernels, sessions)

- 16 tools covering Contents, Kernels, Kernelspecs, and Sessions REST APIs
- File upload/download via UserFile, following the Box upload pattern
- Block with operation dropdown, token auth, and 8 catalog templates
- Registered tools + block, generated docs, bumped API validation baseline

* fix(jupyter): address Greptile review — SSRF guard, upload path ambiguity, silent notebook fallback

- Route uploads through validateUrlWithDNS + secureFetchWithPinnedIP (matches Grafana/1Password pattern) instead of a raw fetch to the user-supplied server URL
- Replace the upload path/trailing-slash heuristic with an unambiguous directory + filename split
- create_file no longer silently writes an empty notebook when notebook content is malformed JSON — it now errors clearly

* fix(jupyter): request content=1 when listing directory contents

Without it, Jupyter Server returns directory metadata with content:
null, so jupyter_list_contents always reported an empty items array.

* fix(jupyter): reject path-traversal segments in Jupyter content paths

encodeJupyterPath now rejects '.'/'..' segments across the whole path
(shared by all 16 tools, not just upload); the upload route returns a
clean 400 when it's hit.

* fix(jupyter): close remaining path-traversal and redirect-credential gaps

- extract the traversal check out of encodeJupyterPath into a shared
  assertion, and apply it to body-only path fields (rename newPath,
  copy copyFromPath, session path) that never flowed through URL
  encoding and so skipped the check
- pass stripAuthOnRedirect to the upload route's secureFetchWithPinnedIP
  call so a malicious Jupyter server can't redirect the PUT to another
  origin and receive the caller's token

* fix(jupyter): also reject percent-encoded traversal segments

A segment like %2e%2e wouldn't match the literal '..' check. Now
decodes each segment before comparing, in addition to the literal
check, so an already-encoded traversal attempt is caught too.

* fix(jupyter): route all 15 remaining tools through an internal proxy for HTTP/private-host support and no redirects

The generic external tool executor blocks plain-HTTP and non-localhost
private-IP hosts by default, so every non-upload Jupyter operation
could fail against typical self-hosted setups (LAN IP, docker
hostname, or even literal localhost on a hosted deployment) even
though the upload route worked via its own internal route.

Added /api/tools/jupyter/proxy (DNS-pinned, allowHttp, maxRedirects: 0)
that mirrors the upstream Jupyter response verbatim, matching the
established pattern for self-hosted-arbitrary-host integrations
(Grafana, 1Password) instead of the generic executor path. Each tool's
request block now posts to the proxy instead of building a direct
external URL; transformResponse and outputs are unchanged since the
proxy response mirrors upstream status/body exactly.

Also switches the upload route from stripAuthOnRedirect to
maxRedirects: 0 — stronger, since it stops the uploaded file body
(not just the token) from ever reaching a redirect target.

* fix(jupyter): validate proxy path at the trust boundary, reject path separators in upload filename

- The proxy route now independently validates the incoming path field
  for traversal segments instead of only relying on tool-side
  validation before the request reaches it — the route is a shared
  internal boundary, not something only our own tool code can call
- The upload route's fileName can come from an advanced override or
  the legacy fileContent path and could itself contain '/' or '\',
  silently nesting the upload deeper than the directory param
  specified. Now rejected outright before joining.

* fix(jupyter): decode the whole path before splitting, not per-already-split segment

A segment like foo%2f..%2fsecret has no literal slash, so splitting on
literal '/' first and decoding each piece in isolation treats it as
one opaque segment and never notices the '..' hiding behind the
encoded slash. Decode the full path once, then split and check every
segment the target server's own single URL-decode pass would see.
2026-07-08 19:35:57 -07:00
Waleed 03b2fd4cbb fix(triggers): validate 10 more webhook/polling triggers against live API docs (#5530)
* fix(slack): guard webhook handlers against null/non-object bodies

handleSlackChallenge, extractIdempotencyId, and formatInput cast the
webhook body to Record<string, unknown> without checking for null first.
handleProviderChallenges runs Slack's handleChallenge unconditionally on
every webhook path before the webhook row is looked up, so a POST with a
literal JSON `null` body crashed with a TypeError instead of degrading
gracefully, unlike sibling providers (e.g. monday.ts) that already guard
this case. Switch all three to isRecordLike, matching the pattern used by
other providers.

* fix(stripe): guard extractIdempotencyId against non-object bodies

Bring the Stripe webhook provider in line with the isRecordLike
convention used by other providers (linear, sendblue, instantly) so a
null/non-object body degrades gracefully instead of throwing on
property access. In practice the only caller already guards against
non-object bodies, so this is defense-in-depth, not a live crash fix.

Adds a colocated stripe.test.ts covering signature verification
(valid/invalid/wrong-secret), event-type filtering, formatInput
pass-through, and extractIdempotencyId including the null-body case.

* fix(github): fix workflow_run event matching and formatInput/output-schema mismatch

isGitHubEventMatch's eventMap was missing an entry for github_workflow_run,
so unknown-trigger fallthrough caused that trigger to fire on every GitHub
event type, not just workflow_run.

The provider's formatInput did a raw passthrough of the webhook body, but
the trigger output schemas rename GitHub's reserved `type` field (a
TriggerOutput meta-key) to `user_type`/`owner_type`, and `description` to
`repo_description` for the repository object. Since formatInput never
performed those renames, the declared output fields never matched real
delivered data. Added alias renaming (keeping the raw keys for back-compat,
matching the existing GitLab work_item_type precedent) plus null-body
guards in formatInput/matchEvent and a content-based extractIdempotencyId
fallback.

* fix(jira): dedupe trigger-type dropdown, guard against null webhook bodies

- All 15 Jira trigger files hand-rolled their own selectedTriggerId
  dropdown subBlock instead of using the shared buildTriggerSubBlocks
  helper. Since blocks/blocks/jira.ts merges every trigger's subBlocks
  into one array, the Jira block ended up with 15 duplicate,
  unconditional selectedTriggerId dropdowns. Refactored every trigger to
  use buildTriggerSubBlocks, with includeDropdown only on the primary
  jira_issue_created trigger (matches the jsm sibling module's pattern).
- Removed the dead, unused fieldFilters subBlock on issue_updated (never
  read by matchEvent/formatInput and has no analog in Jira's own webhook
  admin UI, unlike jqlFilter).
- Guarded all `body as Record<string, unknown>` casts in the provider
  handler (matchEvent, formatInput, extractIdempotencyId) and in
  triggers/jira/utils.ts's extract* helpers with isRecordLike, so a
  malformed/scalar JSON body (e.g. literal `null`) degrades gracefully
  instead of throwing.
- jira_webhook (generic, all-events) trigger's output schema was missing
  sprint/project/version keys that its formatInput branch already
  returns, and duplicated comment/worklog shapes that drifted from the
  shared builders (e.g. comment.body typed as plain string instead of
  ADF json). Now composed from the same buildXOutputs() helpers used by
  the dedicated triggers.

Verified against live Atlassian webhook docs: X-Hub-Signature HMAC
signing and the X-Atlassian-Webhook-Identifier header (stable across
retries) are both real and already correctly implemented/allowlisted;
no changes needed there.

* fix(salesforce): correct setup instructions for Flow HTTP Callout auth

Salesforce Flow's HTTP Callout action requires a Named Credential
(and an External Credential for auth headers) pointing at the target
URL — it cannot call an arbitrary URL with inline headers as the
previous instructions implied. Update both the generic and per-event
setup instructions to walk through creating the External/Named
Credential first, and drop the inaccurate "connectivity checks" claim.

* fix(hubspot): cap advanced filters to stay within HubSpot's per-group limit

HubSpot's Search API rejects any filterGroup with more than 6 filters.
buildUserFilters combines pipeline/stage/owner shortcuts with user-supplied
JSON filters and spread them into both filter groups uncapped; Group B
reserves 2 slots for the timestamp/id tie-break, so as few as 3 shortcuts
plus 2 advanced filters silently broke every poll with an opaque 400 from
HubSpot. Cap combined filters at 4 and warn when truncating.

Added hubspot.test.ts covering buildUserFilters (shortcuts, JSON parsing,
invalid-operator drop, malformed JSON, and the new cap) since the file had
no prior test coverage.

* test(zendesk): add handler tests for webhook trigger signature/idempotency/format-input

Audited the Zendesk webhook trigger (signature verification, event
matching, output-schema mapping, idempotency) against live Zendesk
webhook docs and the repo's trigger conventions. No bugs found — the
existing implementation already correctly uses base64 HMAC-SHA256 over
timestamp+body (not hex), safeCompare, fail-closed auth, the native
event-subscription payload shape (not the admin-configurable
Trigger/Automation payload), and null-safe body handling. Added
colocated tests to lock in this behavior, matching the gitlab/linear
test pattern.

* fix(microsoft-teams): persist subscription expiration, close auth/idempotency gaps

- createSubscription never wrote subscriptionExpiration into providerConfig,
  so the renewal cron's `if (!expirationStr) continue` guard permanently
  skipped every Teams chat subscription — they silently expired after
  ~3 days (Graph's chatMessage max lifetime) and were never renewed.
  Persist it on both initial creation and the existing-subscription reuse
  path.
- verifyAuth only checked HMAC when providerConfig.hmacSecret happened to
  be present, silently accepting unauthenticated requests for outgoing
  webhook triggers if it was ever missing. Fail closed instead.
- extractIdempotencyId/enrichHeaders only handled Graph notification
  payloads (the `value` array shape), so outgoing webhook channel messages
  never got a stable idempotency key and fell back to a random one on
  every delivery. Extend to key off the Bot Framework Activity `id`, and
  guard the parsing with isRecordLike instead of an unchecked cast.
- formatInput declared channelData.teamsTeamId/teamsChannelId in the
  trigger's output schema but never populated them (Teams doesn't send
  those as literal wire keys). Compute them from channelData.team.id /
  channelData.channel.id.
- The block's triggers.available list only listed microsoftteams_webhook,
  hiding the chat subscription trigger from Copilot's block metadata tool
  and other consumers of that list. Add it, matching the Jira/Linear
  pattern for multi-trigger blocks.

* fix(sentry): guard webhook handler against null/non-object body

matchEvent cast body to Record<string, unknown> without a null check,
so a validly-signed webhook delivery with a null (or non-object) JSON
body threw inside the shared webhook loop, aborting processing for
any other webhooks sharing the same path. formatInput/extractIdempotencyId
already tolerated null via `|| {}`/optional chaining but didn't match
the isRecordLike convention used by sibling providers (linear, sendblue,
instantly). Aligned all three methods on isRecordLike and added
regression tests for a null body.

* fix(twilio): guard SMS and Voice webhook handlers against non-object body

matchEvent, extractIdempotencyId, and formatInput cast body directly to
Record<string, unknown> without checking it was actually an object, so a
malformed or non-form-encoded request (e.g. JSON body "null") would throw
instead of degrading gracefully. Use isRecordLike, matching the pattern
already used in linear.ts/sendblue.ts/instantly.ts.

* chore(slack): convert inline rationale comments to TSDoc

Adversarial re-audit of the null-body-guard fix: no correctness issues
found, backwards compatibility confirmed strictly additive. Per repo
convention, converts non-TSDoc inline // comments in slack.ts to TSDoc
blocks on the relevant declarations (formatSlackInteractive,
extractIdempotencyId, formatInput) and drops two low-value inline
comments in slack.test.ts that just restated adjacent assertions.

* test(github): add negative alias test, remove extraneous inline comments

Adversarial re-audit of the workflow_run/formatInput fix: eventMap now
covers all 11 GitHub trigger IDs (verified exhaustively against every
file in triggers/github/), and withGitHubUserTypeAliases only augments
objects shaped like a GitHub user (login+type strings) rather than
renaming every `type` key in the tree. Added a test proving an
unrelated nested `type` field (e.g. an issue label) is left untouched.
Removed inline // comments that only restated the code; folded the
two genuinely non-obvious GitHub semantics (issue_comment firing for
both issues and PRs, closed vs merged pull requests) into the
function's TSDoc.

* chore(jira): drop extraneous inline comments from second-pass re-audit

Removed self-evident line comments in isJiraEventMatch() and the
jira_webhook output-schema composition — the code (variable/function
names) already says what these restated.

* fix(salesforce): add missing permission-set and async-path steps to Flow setup instructions

Re-verified the Named Credential setup instructions against live Salesforce
docs. Two required steps were still missing: (1) the Flow's running user
(Automated Process user for record-triggered flows) needs a permission set
granting External Credential Principal Access, or the callout fails auth
even with a correctly configured Named Credential; (2) record-triggered
flows can only perform callouts on the Run Asynchronously path.

* fix(hubspot): fail loudly instead of silently truncating over-limit filters

Filters within a HubSpot Search API filterGroup are AND-combined, so
silently dropping the last N filters when the combined shortcut +
advanced-filter count exceeded MAX_USER_FILTERS widened the match set
instead of narrowing it — a poll could start matching records the
user's config meant to exclude, firing the workflow on unintended
records with no visible error. That's worse than the original
hard-400 bug, which was at least loud. buildUserFilters now throws
when the combined count exceeds the limit, which pollWebhook's
existing catch turns into a visible markWebhookFailed, matching how
every other misconfiguration in this file (missing objectType,
invalid eventType, corrupt watermark) is already handled.

Re-derived the cap arithmetic against HubSpot's live docs
(developers.hubspot.com/docs/api/crm/search): max 6 filters per
filterGroup, 5 groups, 18 total. Group B reserves 2 slots (filterProperty
EQ + hs_object_id GT tie-break), so MAX_USER_FILTERS = 4 is exact — not
off by one in either direction.

* fix(zendesk): validate subdomain as a bare hostname label

createSubscription/deleteSubscription interpolate the user-supplied
subdomain directly into the Zendesk API URL (`https://${subdomain}.zendesk.com`).
An unvalidated value containing a '/' (e.g. "evil.example.com/x") escapes
the host portion of the URL, redirecting the request — and its Basic-auth
admin credentials — to an attacker-controlled host. Unlike the equivalent
pattern in apps/sim/tools/zendesk (invoked only when a user explicitly runs
a block with their own credentials), this fires automatically on deploy and
undeploy using admin-scoped API tokens, making it the more acute instance of
the pattern. Reject anything but letters, digits, and internal hyphens.

Also drops a few restated-in-code inline comments per repo convention.

* fix(triggers): restore jira fieldFilters as a real feature, fix twilio-voice status collision

jira: the second-pass audit removed the `fieldFilters` subBlock on
issue_updated as "dead code, never read" — true, but that meant the
feature was already non-functional before removal (a UI control
promising field-level filtering that did nothing), and removing it
silently dropped an already-visible control from the merged block UI
(flagged independently by both Greptile and Cursor). Rather than
either reinstating a broken control or leaving it removed, implement
the feature for real: matchEvent now checks the comma-separated field
list against Jira's changelog.items[].field on issue_updated
deliveries, matching Jira's actual webhook payload shape.

twilio-voice: extractIdempotencyId keyed on CallSid alone, so every
status callback for a call (ringing/in-progress/completed/etc) shared
one idempotency key and only the first was ever processed — later
CallStatus transitions were silently dropped as duplicates. Fixed to
include CallStatus in the key, matching the SMS handler's existing
SID:status pattern. Also converted an inline rationale comment in the
SMS handler to TSDoc for consistency with the rest of this cleanup.

* fix(twilio-voice): key idempotency on all callback discriminators, not just status

CallStatus/RecordingStatus/TranscriptionStatus share overlapping values
(e.g. 'completed'), and Gather turns / recording events fan out multiple
distinct deliveries under one CallSid while CallStatus itself stays
unchanged. Build the key from field=value pairs across every known
discriminator (CallStatus, Digits, SpeechResult, RecordingSid,
RecordingStatus, TranscriptionSid, TranscriptionStatus) so callback kinds
can never collide on a shared value, while identical retries still dedupe.

* chore(github-trigger): remove extraneous inline comment

Round 3 re-audit: verified push event handling (commits[].author/
committer, head_commit, pusher) is correctly excluded from the
GitHub-user type-alias walk since those objects lack login+type,
so no output-schema drift. No functional issues found. Removed a
leftover inline // comment in the generic webhook trigger's output
schema per repo comment conventions.

* fix(stripe-trigger): add missing 2025 event types to eventTypes allowlist

invoice.payment_attempt_required and balance_settings.updated shipped in
Stripe's 2025-10-29 API update but were missing from the trigger's
curated eventTypes dropdown despite their categories (Invoices, Balance)
already being represented.

* fix(salesforce-trigger): document required Allow Formulas in HTTP Header checkbox

Setup instructions told admins to add a Named Credential custom header
using a $Credential formula, but never mentioned that the "Allow Formulas
in HTTP Header" checkbox must be checked when adding that header.
Left unchecked, Salesforce sends the literal "{!\$Credential...}" text
instead of evaluating it, so the shared-secret auth silently fails.

* fix(slack): skip block_suggestion payloads instead of wastefully executing workflows

block_suggestion (external select option loading) requires Slack to receive a
synchronous JSON options response within 3 seconds, which this trigger's
async fire-and-forget webhook execution model can never provide. It was
previously routed through the generic interactivity handler like
block_actions/shortcut/view_submission, meaning every keystroke in an
external-select typeahead would silently trigger a full (useless) workflow
execution. Now explicitly skipped via the existing skip mechanism.

* fix(microsoftteams): recreate Teams subscription when renewal PATCH 404s

If every renewal attempt in a subscription's 48h renewal window failed
(revoked consent, prolonged Graph outage), the subscription actually
expired on Microsoft's side and the cron kept PATCHing a deleted
subscription forever, always 404ing, with the webhook silently dead.
Now a 404/410 from the renewal PATCH triggers a POST to recreate the
subscription from the stored chatId, closing the same "never comes
back" failure mode the original bug had, for the narrower case where
renewal itself keeps failing. Also dedupes getCredentialOwner onto the
shared provider-subscription-utils helper instead of a local copy, and
drops a couple of restated-in-code comments.
2026-07-08 19:33:14 -07:00
Waleed 2b09c081f3 hotfix(landing): revert unoptimized removal, compress source images instead (#5528)
PR #5522 removed `unoptimized` from local blog/library/integration cover
images, expecting next/image's runtime optimizer to serve resized
AVIF/WebP. On staging, images broke entirely on /blog - the runtime
optimizer is failing there (root cause still under investigation:
_next/image requests aren't captured by the app's structured logger, so
the underlying error wasn't visible in application logs). Reverting
`unoptimized` immediately restores working images.

In its place, this compresses the actual source files (mozjpeg quality 82
for JPEGs, palette PNG for PNGs) at their EXACT existing pixel dimensions -
verified programmatically per-file (dimension mismatch aborts the write)
and spot-checked visually. This gets the same bandwidth/LCP win the
runtime optimizer was meant to provide, without depending on it:

- 13 blog/library cover images + 3 author avatars + 1 brand logo
- ~1.72MB -> ~1.02MB combined (~41% smaller), zero resolution change
2026-07-08 18:28:20 -07:00
Waleed 8bbe6f55ca feat(chat): replace thinking indicator with gooey loader (#5525)
* feat(chat): replace thinking indicator with gooey loader

Promotes ThinkingLoader from the landing route group to the shared
components/ui barrel and swaps it in for the 4-square color loader
in the chat's PendingTagIndicator. Removes the now-dead
animate-thinking-block keyframe/animation.

* fix(chat): keep a stable Thinking… label instead of cycling phase text

Greptile flagged that phase text repeatedly re-announces to assistive tech
during a single pending stream; a static label avoids the repeated
live-region updates while keeping the gooey shape morph.
2026-07-08 17:57:47 -07:00
Waleed 7bd734ceef fix(landing): fix Core Web Vitals regressions across public marketing pages (#5522)
* fix(landing): fix Core Web Vitals regressions across public marketing pages

- root layout unconditionally rendered next-runtime-env's PublicEnvScript,
  which calls unstable_noStore() and silently forced every route in the app
  dynamic - marketing pages never got static/ISR caching despite their own
  revalidate. Gated it to self-hosted only; hosted now uses a static,
  build-time equivalent (app/_shell/public-env-script.tsx)
- removed real pointer-drag handlers from the hero's decorative workflow
  animation (was draggable despite being aria-hidden)
- disabled dragging/panning on the (currently unmounted) landing-preview
  ReactFlow canvas so it's static-by-default if it's ever wired in
- lazy-mount the Product Demo section's duplicate HeroVisual instance via
  next/dynamic + IntersectionObserver instead of loading it eagerly below
  the fold
- disabled Next.js Link prefetch on always-in-viewport /signup and /login
  CTAs (navbar, hero, mobile nav) so their JS isn't fetched on every
  pageview regardless of whether the visitor clicks
- removed `unoptimized` from local blog/integration images (including the
  priority LCP image on every blog post), letting next/image serve
  resized AVIF/WebP instead of full-size originals

* fix(landing): address review findings on the CWV PR

- revert prefetch={false} on below-fold CTAs (cta.tsx, enterprise.tsx) -
  contradicts the prefetch-on-approach rule this PR itself documents
- restore unoptimized on avatarUrl and the MDX body-image renderer, both of
  which can legitimately hold external URLs outside next.config.ts's
  image remotePatterns allow-list
- simplify handleAnchors to a single block argument now that positions are
  static (the second "live position" argument was always identical to the
  first after the drag-handler removal)
- extract the near-duplicate IntersectionObserver lazy-mount logic shared
  by landing-preview-mount.tsx and product-demo-visual-mount.tsx into a
  single apps/sim/app/(landing)/hooks/use-lazy-mount.ts hook
- import next-runtime-env's own exported PUBLIC_ENV_KEY constant instead of
  a hardcoded string literal, and match its case-insensitive NEXT_PUBLIC_
  filter exactly, removing any drift risk between the two implementations
- drop plain inline comments with no TSDoc home in favor of relying on the
  existing TSDoc/CLAUDE.md documentation

* fix(landing): carry the unoptimized-image fix onto the shared content-*-page components

The blog/library split (#5516) moved the blog post/index/author JSX into
shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while
this branch was in flight, so the original unoptimized removal (verified
local-only ogImage paths for both blog and library content) needs to land
on those shared components instead of the old per-route JSX.

* fix(landing): document ogImage's local-path expectation, fix CLAUDE.md structure doc

- add a one-line comment on ContentFrontmatterSchema.ogImage documenting
  that it's rendered without unoptimized and expects a local path, matching
  the existing avatarUrl comment convention (a reviewer noted ogImage's
  schema is technically unconstrained and seo.ts has an http-prefix branch,
  though all current content is local)
- add the new hooks/ folder to the (landing) CLAUDE.md structure diagram and
  name use-lazy-mount.ts directly in the lazy-mount rule

* fix(landing): escape </script> breakout in the static public-env script

Greptile P1: a NEXT_PUBLIC_* value containing "</script>" would close the
inline script early and could inject markup/script into every hosted page.
Escape "<" in the serialized JSON before interpolating it, matching the
standard JSON-in-script-tag safeguard.
2026-07-08 17:49:35 -07:00
Waleed 1ee98a61f2 fix(triggers): repair broken idempotency dedup across 4 webhook/polling triggers (#5524)
* fix(instantly): add idempotency dedup for webhook deliveries

Instantly webhook deliveries had no extractIdempotencyId, so retried
deliveries were never deduped. Instantly doesn't send a delivery-id
header, so key on email_id when present (unique per email/reply) and
fall back to a content-based key (event_type + campaign_id + lead_email
+ event timestamp) otherwise. Added instantly.test.ts covering auth,
event matching, formatInput/output alignment, idempotency, and the
subscription lifecycle.

* fix(gitlab): add delivery idempotency and fix reserved type-field collision risk

extractIdempotencyId was missing entirely, so GitLab's automatic webhook
retries (triggered after timeouts/5xx, up to 4 consecutive failures before
temporary disable) would re-execute the workflow as a brand-new event.
Added a content-derived fallback key (checkout_sha for push, object
id + updated_at otherwise) since GitLab's X-Gitlab-Event-UUID delivery
header isn't available to extractIdempotencyId (body-only).

Also renamed the Issue Hook payload's object_attributes.type (GitLab
17.2+ work item type) to work_item_type in formatInput and exposed it
in buildGitLabIssueOutputs, since 'type' collides with the TriggerOutput
meta-key. Added detailed_merge_status to the merge request outputs to
match the deprecation of merge_status.

Added apps/sim/lib/webhooks/providers/gitlab.test.ts covering verifyAuth,
matchEvent, formatInput, and extractIdempotencyId.

* fix(gmail): correct labels output type and add formatInput/schema alignment test

The gmail_poller trigger declared its `labels` output as type: 'string'
while the description and actual runtime value (email.labelIds) are a
string array. Changed to type: 'json' to match the shape actually
delivered, consistent with how other triggers type array-of-string
fields.

Added apps/sim/lib/webhooks/providers/gmail.test.ts covering
gmailHandler.formatInput passthrough behavior and a regression check
that every key formatInput can deliver on `email` matches a key
declared in the trigger's output schema.

* fix(linear): tighten replay window, dedupe HMAC verify, add idempotency fallback

- Reuse the shared createHmacVerifier helper for Linear's signature check
  instead of hand-rolling header/secret validation.
- Tighten the webhookTimestamp replay-protection skew from 5 minutes to 60
  seconds, matching Linear's documented recommendation.
- Add extractIdempotencyId as a content-based fallback (type:action:id:
  updatedAt) for when the Linear-Delivery header is unavailable.
- Remove unused teamOutputs/stateOutputs dead code from triggers/linear/utils.ts.

* fix(gitlab): preserve raw object_attributes.type alongside work_item_type

formatInput previously spread the entire raw body, so object_attributes.type
(GitLab 17.2+ work item type) was already reachable via a manual expression
even though undocumented. The earlier fix renamed it to work_item_type in
the delivered data too, which would silently break any pre-existing
workflow referencing the raw path. Keep both keys in the delivered data —
this is plain passthrough data, not schema-constrained, so there's no
reserved-key collision risk in doing so; only the declared *schema* needs
the work_item_type name.

* fix(instantly): differentiate idempotency key by timestamp when email_id repeats

Instantly's is_first field ("whether this is the first event of this
type for the lead") only makes sense if the same event_type can fire
more than once for the same email_id: every open of the same email,
every click, and every reply in a thread all share one email_id
(Instantly's reply_to_uuid). Keying idempotency on email_id alone
collapsed all of those distinct, legitimate deliveries into a single
dedup slot, so every open/click/reply after the first was silently
dropped for the 7-day idempotency TTL. Append timestamp (fixed per
event occurrence, confirmed not to regenerate on retry) to keep the
key both retry-stable and unique per occurrence.

* fix(linear): revert replay-window tightening — retry timestamp semantics unverified

The prior pass tightened LINEAR_WEBHOOK_TIMESTAMP_SKEW_MS from 5 minutes to
Linear's suggested 60 seconds, justified by the claim that webhookTimestamp
is re-stamped fresh on every delivery/retry attempt. That claim is not
stated anywhere in Linear's webhook docs (verified against the raw docs
page directly, including the exact wording around retries and the
webhookTimestamp field) — the docs only say it is "the time when the
webhook was sent" with no mention of retry behavior. A third-party
implementation guide explicitly recommends a 5-minute window instead of
Linear's literal 60s suggestion, citing this same ambiguity.

If webhookTimestamp is actually fixed to the original event time (not
refreshed per attempt), a strict 60s window would silently and permanently
reject every one of Linear's own 1hr/6hr retry deliveries following any
transient outage on our side, since Linear gives up after 3 failed
attempts. Idempotency dedup (Linear-Delivery header / extractIdempotencyId
fallback) already prevents double-processing of replayed/retried
deliveries within a wider window, so the extra replay-protection from
matching the literal 60s suggestion is marginal next to the risk of
dropping real business events. Reverting to the 5-minute window pending
explicit confirmation from Linear on retry timestamp semantics.

* fix(gitlab): include ref in push idempotency key to avoid branch-deletion collisions

GitLab sets checkout_sha to null on branch/tag deletion, so the
extractIdempotencyId fallback (checkout_sha || after) degenerated to the
all-zeros SHA for every deletion. Without ref in the key, two unrelated
branch deletions in the same project produced the same idempotency key,
silently dropping the second (and any subsequent) legitimate trigger
execution as a false duplicate.

* fix(gmail): use 'array' not 'json' for poller labels output type

The prior pass changed the gmail_poller trigger's `labels` output from
'string' to 'json', which is directionally correct (labelIds is a
string array, not a string) but not the precise type. The codebase's
own PrimitiveValueType union has a dedicated 'array' variant, used by
sibling triggers with identical semantics (github issue/PR triggers,
Jira triggers all declare their `labels` field as type: 'array').

Concretely, 'json' vs 'array' diverges in generateMockValue
(apps/sim/lib/workflows/triggers/trigger-utils.ts), which backs the
auto-generated "Event Payload Example" sample payload shown in the
trigger config UI (apps/sim/triggers/index.ts, gated on
trigger.id.includes('poller') — gmail_poller qualifies). 'json' mocks
as a single generic object ({id, name, status}), actively misleading
for a field that is always an array. 'array' mocks as a list, matching
the true shape (Gmail API docs confirm labelIds is a string array on
the Message resource).

No behavioral difference in output-path validation/resolution
(isPathInSchema, generateOutputPaths) since the field declares no
items/properties either way — this is purely a type-declaration
correctness fix, verified against Gmail API docs and cross-checked
against every other output field in poller.ts (all match runtime
shape).

* docs(triggers): correct GitLab retry claim, convert inline comments to TSDoc

GitLab does not automatically retry failed webhook deliveries — a failed
request only counts toward auto-disabling the webhook (4 consecutive
failures = temporary disable, 40 = permanent), and re-delivery only
happens via a manual "Resend Request" (UI or API), which carries the
same webhook-id/Idempotency-Key/X-Gitlab-Event-UUID headers as the
original delivery. Those headers are already in the shared idempotency
service's allowlist and checked ahead of extractIdempotencyId, so the
body-based fallback matters for the rare case those headers are
stripped in transit — not for "automatic retries" as previously stated.
Verified directly against docs.gitlab.com.

Also replaced inline `//` comments across the gitlab/gmail/linear/
instantly provider and trigger files with TSDoc blocks on the relevant
declaration where the reasoning was non-obvious, or removed them
outright where they only restated adjacent code.

* fix(triggers): close two Greptile-flagged crash/collision gaps

instantly: the timestamp-present branch was fixed for the email_id
collision risk, but the no-timestamp fallback still keyed on bare
email_id — same collision risk, unfixed. Return null instead so dedup
is skipped rather than risking a false collision between repeat opens/
clicks/replies on the same email.

linear: extractIdempotencyId cast `body` straight to a Record without
checking it was actually an object first, so a JSON `null` body (no
Linear-Delivery header available) would throw on `b.type` and turn a
malformed payload into a webhook 500 instead of a clean skip.

* fix(linear): guard matchEvent and formatInput against a null body

Found during a final backwards-compat pass, same class of bug Greptile
just caught in extractIdempotencyId: both cast body straight to a
Record without checking it was actually an object, so a genuinely
null/malformed body would throw instead of degrading gracefully.
gitlab.ts (asRecord's `|| {}`) and instantly.ts (isRecordLike checks)
already guard this everywhere; linear.ts was the one inconsistent
provider. Uses the same isRecordLike helper as instantly.ts.

* fix(gitlab): distinguish pipeline lifecycle transitions in idempotency key

Pipeline Hook payloads have no updated_at field (confirmed against
docs.gitlab.com — only Issue/Merge Request/Note hooks reliably include
it), so every lifecycle transition of the same pipeline (pending ->
running -> success/failed) collapsed onto the identical fallback key
and later real transitions were skipped as duplicates. Falls back to
status + finished_at/created_at when updated_at is absent.
2026-07-08 17:33:15 -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
Vikhyath Mondreti 9404bda5c2 docs(forking): workspace forking for enterprises (#5520)
* docs(forking): workspace forking

* update comparison links
2026-07-08 16:00:36 -07:00
Waleed 57ff500c50 feat(comparison): add Custom Blocks as a comparison-page fact (#5519)
- New "customBlocks" row in the platform section: whether a platform
  lets a builder publish a deployed workflow as a reusable, encapsulated
  block for org-wide reuse (Sim's new Custom Blocks feature)
- Researched and sourced an accurate answer for all 20 competitors,
  distinguishing real equivalents from look-alikes (sub-workflow
  composition, custom code nodes, connector wrappers, templates)
2026-07-08 15:42:12 -07:00
Waleed 07c026a917 fix(ashby): repair broken idempotency dedup and clarify duplicate-webhook errors (#5518)
* fix(ashby): repair broken idempotency dedup and clarify duplicate-webhook errors

extractIdempotencyId looked for a webhookActionId field that does not
exist anywhere in Ashby's webhook payload schema (confirmed against the
live OpenAPI spec), so every delivery — including Ashby's own retries —
got a fresh random dedup key and could re-execute workflows. Derive the
key from the affected resource's id plus its updatedAt/decidedAt instead.

Also surface a clear, actionable message when Ashby's webhook.create
rejects a request as a duplicate (seen repeatedly in production logs,
where the outbox retried the same failing subscription for ~23 minutes
before dead-lettering) instead of the generic API error passthrough.

Also add the interview stage `type` field to trigger outputs (present
in Ashby's schema, useful for a stage-change trigger) and fix the
employmentType description to match Ashby's actual enum values.

* fix(ashby): correct offer status enum values in trigger output descriptions

acceptanceStatus listed a non-existent "WaitingOnResponse" value and
offerStatus was missing "WaitingOnApprovalDefinition" — both caught on
a second pass re-checking every enum against Ashby's live OpenAPI spec.

* fix(ashby): harden idempotency key against Greptile-flagged collision risks

- Return null (skip dedup) when application.updatedAt is absent instead
  of collapsing to an empty string, which could collide two distinct
  events sharing an application id onto the same key.
- Drop decidedAt from the offerCreate key. It's populated only after
  the fact, so including it gave a retry of the same delivery a
  different key than the original attempt once the candidate
  responded, defeating dedup. offer.id alone is already stable and
  unique per created offer.

* fix(ashby): fall back to a content fingerprint instead of skipping dedup

Returning null when application.updatedAt was missing avoided false
collisions between distinct events, but also disabled retry dedup
entirely for that payload shape — an Ashby retry would get a random
key from the idempotency service's own fallback and re-run the
workflow.

Extract the fallback-fingerprint helper (sha256 of a stably-serialized
payload) out of salesforce.ts into the shared providers/utils.ts, and
use it in ashby.ts: identical retried bytes hash identically (dedup
still works), while two genuinely different events hash differently
(no false collision).

* chore(ashby): remove inline comments, let names carry the intent

* fix(ashby): fingerprint the full data payload, not just application

Hashing only data.application missed other fields (e.g. offer on
candidateHire) when updatedAt is absent, so two deliveries sharing an
application snapshot but differing elsewhere in data could collide
onto the same idempotency key.

* fix(ashby): rename output field 'type' to 'stageType' to fix build

TriggerOutput reserves the 'type' key for the output's own JSON type
(e.g. 'string'), so a nested field literally named 'type' inside
currentInterviewStage collided with that meta-field and broke the
Record<string, TriggerOutput> cast — passing local type-check (which
turbo was silently serving a stale cached pass for) but failing
Next.js's build-time type check in CI. Renamed to stageType, matching
the existing eventType-style convention used elsewhere for API fields
literally called 'type'.

* fix(ashby): rename currentInterviewStage.type to stageType in delivered data too

Renaming the field in the output schema alone (to fix the TriggerOutput
build collision) left a mismatch: Ashby's real payload still has
currentInterviewStage.type, so a picker/expression using the schema's
declared stageType would resolve to undefined at runtime. formatInput
now renames the field in the actual delivered payload so it matches
what the schema declares.

* fix(ashby): fold offer id into the idempotency key when present

When application.updatedAt is present, the key ignored sibling data
fields — a candidateHire delivery carries both application and offer,
so two deliveries sharing an application snapshot could collide even
though they concern different offers. Append offer.id to the
discriminator when present; it's the only sibling object Ashby's
schema ever pairs with application.
2026-07-08 15:34:18 -07:00
Waleed 1236474efb fix(rich-markdown-editor): eliminate empty-block / boundary-key structural bugs (#5517)
* fix(rich-markdown-editor): don't split a list when deleting an empty item

Backspacing an emptied list item in the middle of a list used ProseMirror's default lift, which pulls
the item out into a top-level paragraph — splitting one list into two and stranding an empty paragraph
(a visible gap). Backspace at the start of an empty list item now joins into the previous block instead,
removing the item and keeping a single list. Covers bullet, ordered, and task lists.

* fix(rich-markdown-editor): generalize empty-wrapped-block boundary keys

Replace the joinBackward empty-list-item Backspace branch with a walk-up-and-delete
that removes the whole emptied wrapper, and extend it to task items and blockquotes.
joinBackward left a stray empty paragraph inside the previous item (flat lists) or
no-op'd entirely on nested items (leaving them stuck); blockquotes were never handled
and split in two. Add an Enter handler so an empty non-trailing list/task item is
removed rather than split into two lists around a stranded, non-round-trippable empty
paragraph; a trailing empty item still exits the list via the default.

Covers bullet/ordered/task/blockquote at every position; non-empty items and the
double-Enter list-exit are unaffected.

* fix(rich-markdown-editor): isolate verbatim block nodes from boundary joins

footnoteDef and rawHtmlBlock hold exact source text but were neither isolating nor
atom, so a single Backspace/Delete at their boundary let ProseMirror's default join
merge their raw markdown into an adjacent paragraph as HTML-escaped prose — silently
destroying the node and corrupting saved markdown. Mark the block variants isolating
so boundary keys can't cross their edge. Round-trip and in-place editing are unchanged.

* fix(rich-markdown-editor): escape leading block markers in paragraph serialization

A paragraph beginning with #, -, +, 1., 1), or a bare --- serialized unescaped, so it
silently re-parsed into a heading / list / thematic break on the next load (reachable
via type-a-marker then undo). The upstream serializer escapes inline delimiters (* _ `
[ ] ~, so * bullets and > quotes already round-trip) but not these block-starting
markers. BlockSafeParagraph wraps the paragraph renderer with a leading-marker guard;
escaping is idempotent (parsing consumes the backslash) and never over-escapes non-markers
like #hashtag or -5.

* fix(rich-markdown-editor): strip leading paragraph indent; keep empty paragraphs &nbsp;-free

A paragraph beginning with a 4-space/tab indent re-parsed as an indented code block on the
next load. Leading whitespace never renders in a paragraph (CommonMark strips up to three
leading spaces; four or more become code), so BlockSafeParagraph now strips it — lossless
and idempotent. Composes with the existing leading-marker escaping (e.g. '   # x' → '\# x').

Also locks in that consecutive empty paragraphs serialize via blank lines rather than the
upstream '&nbsp;' marker: replacing StarterKit's Paragraph already dropped the &nbsp; path,
and the round-trip preserves the empty-paragraph count without tripping the read-only
safety gate (which flags &nbsp; as a stable-loss pattern). Added tests for both.
2026-07-08 15:14:15 -07:00
Theodore LiandClaude Opus 4.8 d30eb36d9a docs(enterprise): add Custom Blocks page (#5515)
* docs(enterprise): add Custom Blocks page

Document publishing a deployed workflow as a reusable org-wide block:
publishing flow, common uses, using a block, managing, and self-hosted
setup, with UI screenshots. Register in the enterprise sidebar and wire
the settings docsLink.

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

* docs(enterprise): drop explicit Enterprise framing, note access control

The page's Enterprise placement is implicit, so remove the "Enterprise
feature" callout and plan mentions. Add that custom blocks can be
allowlisted per permission group via Access Control.

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

* docs(enterprise): remove Self-hosted setup section from Custom Blocks

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:36:45 -04:00
Justin BlumencranzandClaude Fable 5 fa82acd645 fix(workflow): render agent tool chip names from static sources, not stored title (#5501)
* fix(workflow): render agent tool chip names from static sources, not stored title

Tool entries in an agent block's inputs carry a mutable `title` in workflow
state, which copilot edits (edit_workflow) could rewrite to change what the
UI displays. Derive chip names from static/canonical sources instead: the
block registry name for integration tools (raw type id if unregistered),
the custom-tool record over the stored snapshot, the live MCP tool name,
and a static literal for workflow-as-tool. The stored `title` still persists
in state; the UI just no longer reads it for registry-backed tools.

Covers both render paths: the expanded tool-input chips and the collapsed
canvas summary (resolveToolsLabel, also used by workflow preview).

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

* fix(workflow): cover workflow-as-tool and custom-tool fallbacks in static naming

Address review findings: resolveToolsLabel now returns the static
'Workflow' label for workflow/workflow_input entries instead of falling
through to the stored title, matching the panel chip; the panel's
custom-tool name priority now matches resolveToolsLabel (record title,
record schema function name, then stored title); and the display-name
comment no longer overstates the guarantee.

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

* fix(workflow): resolve panel chip names via unfiltered block registry

The panel resolved integration tool names through toolBlocks, which is
permission- and toolbar-filtered, so a stored tool whose type was
filtered out of the picker fell back to the raw type id while the
canvas summary (getBlock) still showed the registry name. Use getBlock
directly for the display name so both paths agree; toolBlocks still
drives the chip icon, params, and picker.

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

* fix(workflow): resolve MCP tool names from live server data in canvas summaries

resolveToolsLabel preferred the stored title for mcp entries, so canvas
and preview summaries could show a state-edited name while the config
panel showed the live MCP tool name. Accept an optional mcpTools list
(matched by toolId, same as the panel) and pass the already-fetched MCP
tool data from workflow-block; the stored title remains the fallback
while server data is unavailable.

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

* refactor(workflow): single shared tool-name resolver; fix overlay, fallback, and search regressions

Consolidate the per-surface name logic into one resolveStoredToolName in
display.ts (canonical source -> stored title -> raw type id) used by the
panel chips, the canvas summary, and the search indexer, fixing the
regressions the per-renderer patches introduced:

- custom-block tools (async registry overlay) no longer render raw
  custom_block_<uuid> ids pre-hydration or after deletion; the canvas
  memo and panel picker now subscribe to the overlay version so labels
  recompute when custom blocks hydrate
- unresolvable block types fall back to the stored title before the raw
  type id, instead of always showing the id
- the search indexer now indexes the resolved display name (the title
  entry was already read-only), so search text and highlights match what
  the chips render instead of the mutable stored title
- redundant hardcoded 'Workflow' labels removed (registry names cover
  workflow/workflow_input); MCP lookups use a shared Map keyed by
  composite tool id instead of per-entry array scans; panel chip chrome
  falls back to the unfiltered registry for picker-hidden types

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

* fix(workflow): thread custom-tool and MCP data into the search indexer; fix blockConfigs type

The search index now resolves tool names with the same data as the chip
renderers: the single caller passes customTools and a live MCP name map
into indexWorkflowSearchMatches, so indexed names match the panel for
custom-tool references and MCP entries, not just registry-backed types.
Also declare the optional name on WorkflowSearchIndexerOptions
blockConfigs entries, fixing the next build type error.

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

* fix(workflow): recompute search index when the custom-block overlay hydrates

The matches memo resolves tool names through getBlock, so it must carry
the overlay version dep like the other getBlock-derived memos; without
it, find-in-workflow could index stale custom-block names until an
unrelated dep changed.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:13:06 -07:00
Waleed 004955d934 refactor(blog): split AEO/GEO content into a new /library section (#5516)
* refactor(blog): split AEO/GEO content into a new /library section

Blog was mixing hand-written editorial posts with listicle/comparison/how-to
posts optimized for answer engines. Extract a generic content engine
(lib/content) shared by both sections, move the 6 AEO/GEO posts into a new
/library route tree, and keep /blog editorial-only.

- lib/content: generic registry factory, MDX components, SEO builders
- lib/blog + lib/library: thin per-section instantiations over the shared engine
- app/(landing)/library: mirrors the blog route tree via shared Content*Page components
- app/sitemap.ts, app/robots.ts, navbar: updated for the new section
- next.config.ts: permanent redirects from moved /blog/<slug> URLs to /library/<slug>

* fix(blog): thread route id through buildAuthorMetadata for canonical URL

Author pages for unknown/unmatched ids fell back to author?.id, which is
undefined when no post matches — collapsing canonical/OG URLs to
{basePath}/authors/ instead of {basePath}/authors/{id}. Pass the route id
explicitly so canonical always resolves correctly.

* fix(blog): address Greptile review — RSS lastBuildDate, shared author cache, dead-export docs

- RSS lastBuildDate now uses updated ?? date so edits after publication are
  reflected, matching the per-item pubDate semantics (blog + library)
- Author JSON is now cached once per authorsDir at module scope instead of
  once per registry instantiation, since blog and library point at the same
  directory
- Documented getNavPosts as reserved-but-unwired, matching the existing
  PLATFORM_MENU/SOLUTIONS_MENU convention in the navbar

* fix(blog): dedupe featured posts from ContentIndexPage pagination pool

A featured post older than POSTS_PER_PAGE would appear both on page 1's
featured row (sorted to the front) and again on its natural date-sorted
page, since only page 1 excluded featured posts from the remaining list.
Carve featured posts out of the paginated pool up front so pagination stays
consistent and duplicate-free across all pages.

* fix(blog): RSS lastBuildDate reflects the whole feed, not just the newest item

lastBuildDate only looked at items[0]'s updated/date, so revising an older
post already in the feed wouldn't advance it. Extract the max-across-posts
logic sitemap.ts already had into a shared lib/content/utils helper
(latestModified) and use it in both RSS routes and the sitemap, so a
revision to any feed item is reflected.
2026-07-08 13:55:54 -07:00
Theodore LiandClaude Opus 4.8 4cd1461b9e feat(custom-blocks): org-member visibility, workspace-admin-gated management (#5507)
* fix(settings): let workspace admins reach the Custom blocks settings page

The Custom blocks page lives in the Enterprise nav section, which the
sidebar filter hides unless the user is an org admin/owner. But the
server authz for managing custom blocks is source-workspace admin
(hasWorkspaceAdminAccess), not org admin — so a workspace admin who
could manage a block via the API couldn't even reach the page.

Add an `allowNonOrgAdmin` nav flag that exempts an item from the org
admin/owner requirement (the plan/hosted entitlement still applies; the
page enforces its own per-resource authz), and set it on custom-blocks.
Other Enterprise items (access control, audit logs, SSO, data
retention/drains, whitelabeling) stay org-admin-only.

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

* feat(custom-blocks): any org member views; create gated on workspace admin

Split visibility from management to match the org-wide nature of custom
blocks:
- View: any org member (the nav item is now plan-gated, not org-admin).
- Create: the "Create block" action shows only when the user is admin of
  a workspace in the current org, matching the publish route's
  source-workspace admin authz.
- Source picker: lists only workspaces the user administers (in the
  current org); the default selection snaps to an eligible workspace when
  the current one isn't one they can publish from.

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

* feat(custom-blocks): read-only detail for non-managers + fill image icon

- Add the source workspaceId to the block wire type and gate the detail
  view on it: a viewer who isn't an admin of the block's source workspace
  gets a read-only view — no Save/Discard, no Delete, all fields disabled.
  Matches the server authz (edit/delete require source-workspace admin),
  so the UI no longer dangles buttons that 403.
- SettingsResourceRow: add an opt-in `iconFill` so uploaded image icons
  fill the tile edge-to-edge instead of clamping to 20px; glyph svgs still
  normalize to 20px. Custom blocks list opts in.

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

* fix(custom-blocks): include workspaceId in publishCustomBlock return

The publish path builds a CustomBlockWithInputs literal; it was missing
the workspaceId field added to the interface, breaking the type check.

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

* fix(custom-blocks): don't flag create as dirty when source auto-snaps

The create dirty-check compared selectedWorkspaceId against the URL
workspace, but the source picker now auto-snaps to the first workspace
the user can publish from — which may differ. Compare against that
eligible default so opening/discarding the create flow doesn't show a
false unsaved-changes state. (Bugbot)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:51:17 -04:00
Vikhyath MondretiandSiddharth Ganesan 4a80374c03 improvement(forking): unlink, settings page migration, deployed chats, public apis, external mcp servers/tools, special subblocks (#5505)
* bad checkpoint

* stash

* add unlink, move UI to settings pages

* fix edge cases

* remove dead code

* remove migration 0255 ahead of staging merge (regenerated after)

* regenerate workflow_mcp_server enum migration on top of staging (0257)

* fix tests

* more tests

* address comments

* consolidate fork migrations into 0257 (enum value + activity metadata indexes)

* acquire MCP server locks before reads in attachment reconcile (TOCTOU)

* move into ee folder + ui perm gates

* use randomInt from @sim/utils/random for chat identifier suffix

---------

Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
2026-07-08 12:15:28 -07:00
Waleed 767307177e fix(comparison): correct stale and unsourced claims on comparison pages (#5513)
* fix(comparison): correct stale and unsourced claims on comparison pages

- Remove stale "Templates" references from the LangChain page and
  related blog posts (feature was removed platform-wide)
- Re-verify every sourced claim across all comparison-page profiles
  (Sim's own facts and all 20 competitor profiles) against live
  sources; fix dead links, renamed products, outdated figures, and
  claims no longer supported by their citation
- Fix a stale sidebar permissions reference in the enterprise
  access-control docs

* fix(comparison): use consistent URL format for n8n AI Workflow Builder citation
2026-07-08 12:11:54 -07:00
Waleed 117fa66ec7 fix(providers): sync xai defaultModel with grok-4.5 recommended flag (#5512) 2026-07-08 11:52:23 -07:00
Waleed 93481e4408 feat(providers): add xAI grok-4.5 model (#5511)
* feat(providers): add xAI grok-4.5 model

* fix(providers): correct grok-4.5 release date to API-availability date
2026-07-08 11:45:54 -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 099f525d50 improvement(knowledge): open document tags from row context menu (#5510)
* improvement(knowledge): open document tags from row context menu

The document row context menu had a Tags entry that only navigated to the document detail page, requiring another click through the breadcrumb dropdown to actually edit tags. It now opens the tag editor directly, and shows even when the document has no tags yet so a first tag can be added.

* fix(knowledge): gate document tags menu item on edit permission

Matches the existing disableRename/disableDelete/disableToggleEnabled pattern; the document detail breadcrumb already hides its Tags entry for non-editors the same way.

* fix(knowledge): derive tags modal document data from live list cache

The modal was fed a frozen document snapshot taken at right-click time. After a save, the mutation only invalidates the single-document and KB-detail queries (not the documents list query), so the modal's own sync effect rebuilt tags from the stale snapshot and could revert or drop the just-saved value. Track only the document id and look it up from the same documents array updateDocument() patches, so the modal always sees current data.
2026-07-08 11:06:31 -07:00