mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
43e61a1eafe9ab305bc4fca01ea9780cebbf756d
5362
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43e61a1eaf |
fix(enrichments): remove Icypeas providers and show Running on in-flight cells (#5572)
* fix(enrichments): remove Icypeas providers and show Running on in-flight cells * fix(enrichments): keep previous value visible while an enrichment cell reruns |
||
|
|
5d3809a0e3 |
feat(tiktok): add tiktok trigger, block (#5504)
* feat(tiktok): add TikTok integration Adds TikTok as a full OAuth-based integration: provider registration (with TikTok's comma-separated scope and client_key requirements), 9 tools covering profile info, video listing/querying, creator info, direct video/photo posting (URL or file upload), inbox drafts, and post status polling, plus the TikTok block, icon, and generated docs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): add avatarFile output to Get User Info Adds a file-typed avatarFile output (sourced from the largest available avatar URL) alongside the existing string avatar fields, so the profile picture can be materialized as a UserFile and chained into file-consuming blocks (e.g. attached to an email), per PR review feedback. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): lower upload memory cap, drop redundant avatar string outputs Cap the file-upload video buffer at 250MB instead of TikTok's 4GB ceiling — relaying that much through this server's memory per request isn't safe under concurrent load, and larger files can still go through the PULL_FROM_URL path, which never buffers on our server. Also drop the now-redundant avatarUrl/avatarUrl100/avatarLargeUrl string outputs from Get User Info in favor of the file-typed avatarFile output alone, since the feature is unreleased and the raw URL is still reachable via avatarFile.url. Cover image URLs on List/Query Videos are confirmed to be signed, expiring TikTok CDN links; left as strings (no file-output conversion path exists for fields nested inside array items) but documented the expiry behavior more clearly. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(ci): bump API validation route-count baseline for TikTok publish-video route The TikTok integration adds one new Zod-backed internal API route (app/api/tools/tiktok/publish-video), which trips the route-count ratchet in check-api-validation-contracts.ts. Bumping totalRoutes and zodRoutes from 917 to 918 (nonZodRoutes stays 0) to acknowledge the new route is properly validated. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(tiktok): drop unused avatar_url_100 from default user fields After removing the avatar string outputs, avatar_url_100 was still requested from TikTok's user info endpoint but never surfaced anywhere. Removed it from the default field list and the field descriptions, and noted that avatar_url/avatar_large_url feed the avatarFile output. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): stop returning raw 'credential' subBlock id from tools.config.params The block's params function built a local `credential` variable from params.oauthCredential and returned it under the key `credential` in every switch case. That literal token is the raw subBlock id, which is deleted after canonical transformation into `oauthCredential` — the blocks.test.ts canonical-param-validation suite flags any params function that still references it. It was also redundant: oauthCredential is already part of the base resolved inputs, which the executor merges into the tool call before config.params overrides are applied, so the OAuth token resolution (which reads contextParams.oauthCredential) worked regardless. Removed the explicit credential plumbing, matching the convention already used by other OAuth blocks like dropbox.ts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): send empty JSON body on Query Creator Info POST query_creator_info had no request.body function, and formatRequestParams() only attaches a body when tool.request.body is defined at all — so despite sending Content-Type: application/json, the request went out with no body whatsoever. Added body: () => ({}), matching the convention already used by other parameterless-POST tools in this codebase (Google Vault, Supabase, Square, Gmail, etc.). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): stop dropping valid zero values in optional numeric fields cursor, photoCoverIndex, and videoCoverTimestampMs all used a truthy check (params.x && {...}) to decide whether to include an optional numeric override, which drops a legitimate 0 (first page has no cursor issue aside, photoCoverIndex 0 is TikTok's own default cover photo, and timestamp 0 is a valid first-frame cover). Switched to explicit undefined/empty-string checks, matching the !== undefined convention the underlying tools already use. In today's resolution pipeline these fields always arrive as strings (even chained block references get stringified by the template resolver), and a non-empty string like "0" is truthy, so this wasn't actively broken end-to-end - but it was relying on that subtlety rather than being correct by construction, and was inconsistent with the tools' own undefined checks. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tiktok): accept newline-separated video IDs in Query Videos videoIds is a long-input (multiline textarea), the same widget used for the newline-separated photoImages field on this block, but its parser only split on commas. Entering one ID per line - the natural pattern for a multiline field, and the one already used elsewhere on this block - produced a single concatenated garbage string instead of an array, so TikTok's query would fail or return nothing. Now splits on commas or newlines, and updated the placeholder/description to reflect both formats. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(tiktok): add app-level webhook ingress and triggers * fix(tiktok): only count actually queued webhook executions Co-authored-by: Cursor <cursoragent@cursor.com> * chore(tiktok): bump API validation baseline for staging merge Co-authored-by: Cursor <cursoragent@cursor.com> * cleanup code * fix type issues * misc code cleanup * remove photos and add upload for videos * move shared video output properties to types.ts so docs generation resolves them Co-authored-by: Cursor <cursoragent@cursor.com> * hide TikTok from toolbar and docs until the integration is ready to ship Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): ratchet API validation baseline to 924 after staging merge Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai> |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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). |
||
|
|
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
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
cc7a6a82f8 | fix(custom-blocks): stop draft-load sanitization deleting consumer-typed input values (#5551) | ||
|
|
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. |
||
|
|
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`. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 `&`/`<`/`>` 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). |
||
|
|
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". |
||
|
|
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.
|
||
|
|
b09e0c0d3b | feat(providers): add OpenAI GPT-5.6 Sol, Terra, and Luna models (#5537) | ||
|
|
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. |
||
|
|
fea3391015 |
improvement(forking): minor ui improvements (#5533)
* improvement(forking): minor ui improvements * fix warning |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
9404bda5c2 |
docs(forking): workspace forking for enterprises (#5520)
* docs(forking): workspace forking * update comparison links |
||
|
|
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) |
||
|
|
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. |
||
|
|
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 -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 ' ' marker: replacing StarterKit's Paragraph already dropped the path, and the round-trip preserves the empty-paragraph count without tripping the read-only safety gate (which flags as a stable-loss pattern). Added tests for both. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
117fa66ec7 | fix(providers): sync xai defaultModel with grok-4.5 recommended flag (#5512) | ||
|
|
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 |