Commit Graph
3514 Commits
Author SHA1 Message Date
Waleed d643be0b93 feat(triggers): add GitLab, PagerDuty, and Zendesk webhook triggers (#5150)
* feat(triggers): add GitLab, PagerDuty, and Zendesk webhook triggers

Add webhook trigger support for three integrations that previously had
blocks but no triggers:

- GitLab: push, merge request, issue, pipeline, comment, and all-events.
  Verifies the X-Gitlab-Token secret token; filters by object_kind.
- PagerDuty: incident triggered/acknowledged/resolved/escalated/reassigned
  and all-events. Verifies X-PagerDuty-Signature (HMAC-SHA256 over raw body,
  comma-separated rotation); idempotency on event id.
- Zendesk: ticket created/status changed/comment added/priority changed and
  all-events. Verifies X-Zendesk-Webhook-Signature (base64 HMAC-SHA256 over
  timestamp+body); idempotency on event id.

Register GitLab's X-Gitlab-Event-UUID delivery header for webhook
idempotency dedup.

* fix(triggers): scope webhook secrets to owner and add Zendesk replay protection

Address review feedback:
- Add paramVisibility: 'user-only' to the webhookSecret fields for GitLab,
  PagerDuty, and Zendesk so signing secrets are scoped to the credential
  owner and not exposed to workspace collaborators (repo convention).
- Reject Zendesk deliveries whose signed timestamp is more than 5 minutes
  from now, closing a replay window once an event id ages out of the
  idempotency cache. The X-Zendesk-Webhook-Signature-Timestamp header is
  ISO-8601, so it is parsed with Date.parse (matches the Slack handler's
  skew-check convention).

* feat(triggers): auto-register GitLab, PagerDuty, and Zendesk webhooks

Replace the manual-registration model with automatic webhook creation on
deploy and cleanup on undeploy, via createSubscription/deleteSubscription
on each provider handler:

- GitLab: POST /projects/:id/hooks with a Personal Access Token; generates
  the secret token (stored for X-Gitlab-Token verification) and enables only
  the event flags for the selected trigger. Deletes the hook on undeploy.
- PagerDuty: POST /webhook_subscriptions (account-scoped) with a REST API
  key; captures delivery_method.secret (returned only on create) for
  X-PagerDuty-Signature verification. Deletes the subscription on undeploy.
- Zendesk: POST /api/v2/webhooks with native event subscriptions, then GET
  /webhooks/:id/signing_secret for X-Zendesk-Webhook-Signature verification.
  Deletes the webhook on undeploy.

Trigger config now collects the provider credentials (user-only) instead of a
pasted signing secret; the signing secret is generated or fetched and stored
in providerConfig by the orchestration layer (no route/deploy changes).

* fix(triggers): fail closed on missing webhook secret and clean up Zendesk orphans

Address review feedback on the auto-registration changes:
- verifyAuth now rejects (401) when webhookSecret is absent for GitLab,
  PagerDuty, and Zendesk. Since the secret is generated/fetched during
  auto-registration and stored before the webhook can receive deliveries, a
  missing secret indicates misconfiguration and must fail closed rather than
  skip signature verification. Adds an opt-in requireSecret flag to
  createHmacVerifier (default off, preserving behavior for other providers).
- Zendesk createSubscription now deletes the just-created webhook if the
  follow-up signing-secret fetch fails, avoiding an orphaned subscription in
  Zendesk when setup cannot complete.

* fix(triggers): clean up GitLab and PagerDuty webhooks on failed setup

Extend the orphan-prevention fix to the remaining providers. When a create
call succeeds but post-create validation fails, the created webhook is now
deleted before throwing:
- GitLab: if the create response can't be parsed for its hook id, the hook is
  located by its URL and deleted.
- PagerDuty: if the subscription response lacks an id or signing secret, the
  subscription is deleted (by id when known, otherwise located by URL).

Both cleanups are best-effort and never throw.

* docs(triggers): note GitLab tag_push only flows through the all-events trigger
2026-06-20 15:00:59 -07:00
Waleed aa57f10b44 fix(auth): close nOAuth account takeover via email-based OAuth linking (#5156)
* fix(auth): close nOAuth account takeover via email-based OAuth linking

Restrict the unauthenticated sign-in endpoints to first-party login
providers, trim trustedProviders to providers that verify email
ownership, and stop hardcoding emailVerified for multi-tenant Microsoft
and Salesforce connectors.

* test(auth): cover Microsoft id-token emailVerified derivation

Extract the Microsoft ID-token email-verification logic into a pure
deriveMicrosoftEmailVerified helper and add unit coverage for explicit,
verified-claim, partial, absent, and malformed Azure AD claim
combinations.

* fix(auth): check the provider field the sign-in handler actually uses

The allowlist guard resolved the provider with `provider ?? providerId`,
but Better Auth reads `provider` on /sign-in/social and `providerId` on
/sign-in/oauth2. A request to /sign-in/oauth2 with an allowed `provider`
and a blocked `providerId` could pass the guard while the handler started
OAuth for the blocked connector. Resolve the field per path via
getRequestedSignInProviderId so the guard checks the same field the
handler acts on.
2026-06-20 15:00:41 -07:00
Vikhyath Mondreti 82cb324638 improvement(access-controls): default workspace experience includes all members (#5153)
* improvement(access-controls): default workspace experience includes all members

* update ui

* address comments

* improve copy

* address zero-member edge case
2026-06-20 14:24:04 -07:00
Waleed 2f7d60745a fix(uploads): close multipart storage-quota bypass via quota-exempt contexts (#5155)
The multipart endpoint accepted the quota-exempt public-asset contexts
(og-images, profile-pictures, workspace-logos), which skip checkStorageQuota,
letting any authenticated writer open arbitrarily large upload sessions that
never count against their plan limit.

These contexts have no large-file flow: their client hooks hard-cap uploads at
5MB (image-only) and the direct-upload strategy only uses multipart above 50MB,
so they always route through the presigned endpoint. Remove them from
ALLOWED_UPLOAD_CONTEXTS (joining logs) so every context the multipart endpoint
serves is quota-enforced.
2026-06-20 14:07:24 -07:00
Waleed 83dc806da8 fix(file-decompress): enforce decompression caps on inflated stream, not declared zip size (#5154)
* fix(file-decompress): enforce decompression caps on inflated stream, not declared zip size

* fix(file-decompress): destroy inflate stream on error to avoid resource leak
2026-06-20 14:06:41 -07:00
Vikhyath Mondreti 35a7bf61d2 fix(executor): stop HITL error edges from firing on successful resume (#5152)
* fix(executor): stop HITL error edges from firing on successful resume

* add comments
2026-06-20 12:41:44 -07:00
Waleed 3ebb9a5029 feat(connectors): add Google Meet knowledge base connector (#5149)
* feat(connectors): add Google Meet knowledge base connector

Syncs Google Meet meeting transcripts into a knowledge base via the Meet
REST API v2. Lists conference records, fetches transcript entries lazily
per meeting (contentDeferred), resolves speaker display names, and maps
participants/duration/meeting-date tags. OAuth via the existing google-meet
provider (meetings.space.readonly).

* fix(connectors): finalize Google Meet transcripts before indexing

- Only index a meeting once every transcript is FILE_GENERATED, so a
  partial transcript is never stored under an endTime-keyed hash that
  would never refresh
- Sort merged transcript entries by start time to preserve chronology
  across multiple transcripts in one conference

* refactor(connectors): dedicated TRANSCRIPTS_PAGE_SIZE constant for Meet transcripts

* fix(connectors): only flag Meet listing capped when cap truncates source

Previously listingCapped was set whenever the fetched count reached
maxMeetings, even when the API returned every record and no next page
existed. That suppressed the sync engine's deletion reconciliation when
the cap happened to equal the true source size. Now flag only when more
pages remain or records were dropped from the page.
2026-06-19 23:35:33 -07:00
Waleed ce283fa1c9 feat(scheduled-tasks): expose Google Calendar-style recurrence options (#5146)
* feat(scheduled-tasks): expose Google Calendar-style recurrence options

Add a per-day weekly toggle (repeat on arbitrary weekdays), monthly
nth-/last-weekday anchoring, and a yearly frequency to the scheduled
task modal, closing the gap with a calendar app's recurrence picker.

The recurrence UI compiles to cron, so this is front-end only: croner
already speaks the nth/last-weekday (#/#L) syntax, and the display path
normalizes #L to cronstrue's L so labels read "last Monday" not "null".

* fix(scheduled-tasks): preserve monthly anchor when reselecting Monthly

Selecting Monthly from the frequency dropdown hard-reset monthlyMode to
day-of-month, silently dropping a previously chosen nth-/last-weekday
anchor when switching cadence away and back. Preserve the existing mode
on reselect, mirroring how the last recurring cadence is restored across
the recurring toggle.

* fix(scheduled-tasks): fold 5th-occurrence monthly into last-weekday; align weekday-digit parsing

Address review edge cases in the monthly recurrence anchors:

- The picker no longer offers a fifth weekday (a 5th occurrence is
  always the month's last), and recurrenceToCron clamps any nth-weekday
  that resolves to a 5th occurrence to #L — so a launch date drifting to
  day 29-31 can never emit #5 and silently skip months without one.
- cronToRecurrence accepts croner's alternate Sunday digit (7) for the
  #/#L monthly anchors, matching parseCronToHumanReadable's normalizer;
  externally-authored 7#L crons now round-trip (canonicalized to 0#L).
- #5 crons are left as custom pass-through so their month-skipping
  behavior is preserved verbatim rather than rewritten.
2026-06-19 23:35:02 -07:00
Theodore Li 1248f8eef4 fix(files): only show Share in context menu for files, not folders (#5147) 2026-06-19 22:35:25 -04:00
Theodore Li 3b78436ae3 feat(pii): gate data retention PII redaction behind feature flag (#5144)
* feat(pii): gate data retention PII redaction behind feature flag

* fix(pii): evaluate pii-redaction flag globally with no org/user context
2026-06-19 22:18:00 -04:00
Waleed ecbe1919d8 feat(files): inline rich markdown editor (#5133)
* feat(files): inline rich markdown editor

Replace the raw/preview split for markdown files with a Linear-style inline WYSIWYG editor (TipTap/ProseMirror): bubble + slash menus, code-block language picker with Prism highlighting and line-wrap, resizable images (HTML <img>), GFM tables, and frontmatter held byte-exact out of band.

A round-trip preflight gate (decided once per open) falls back to the raw Monaco editor for any file that can't be edited losslessly, so the rich editor never silently corrupts a file.

* fix(files): chain autosave unmount flush after in-flight save

The unmount flush no longer fires a concurrent PUT alongside an in-flight save; it awaits the in-flight save and then writes the latest content sequentially, so an out-of-order completion can't clobber newer edits with a stale snapshot (addresses Cursor Bugbot).

* fix(files): read pasted images from clipboard items, not just files

Some browsers expose a pasted or copied image only via DataTransfer.items (with an empty files list), so screenshot paste was silently ignored. extractImageFiles now falls back to items; moved to a testable module with unit tests (addresses Cursor Bugbot).

* fix(files): destroy round-trip probe editor on serialization error

Wrap the probe serialize() in try/finally so the throwaway Editor is always destroyed even if setContent/getMarkdown throws (addresses Greptile). Adds a test proving PipeSafeTable escapes only interior cell pipes, not structural delimiters.

* fix(resource): hold breadcrumb nav latch across the route swap

scheduleClose fired on the pointer/focus exit that immediately follows a click-to-navigate and was clearing the reopen latch before the route swapped, letting the popover flash back open. The latch is now released by a short timer instead (addresses Cursor Bugbot).

* chore(files): drop platform references and non-essential inline comments

* fix(files): scope inline markdown editor to the files view

The mothership preview was routing streaming markdown through the inline editor path: it showed Monaco during streaming (previewMode fell back to 'editor') and lost the streamed content on the TextEditor→MarkdownFileEditor swap (the TextEditor unmounted before it could reconcile + autosave). The inline rich editor is now opt-in via a FileViewer prop that only the files view sets, so the mothership keeps its raw/preview streaming editor and persists as before.

* fix(mothership): use the inline markdown editor in the chat resource view

Idle markdown in the chat resource view now renders the single-surface inline editor (no raw/split/preview pencil toggle), matching the files view. While the agent streams, FileViewer forces the rendered preview instead of Monaco, and the streamed file persists via the agent's server write + the existing content-query invalidation on tool completion — so the idle editor refetches the persisted content.

* refactor(files): collapse the duplicate raw-editor fallback branch in the markdown gate

* fix(mothership): swap to the inline editor once a file preview finishes streaming

The preview session keeps status='complete' and previewText after streaming ends, so streamingContent stayed defined and the file stuck on the read-only rendered preview. Treat content as streaming only while status==='streaming'; once complete the EmbeddedFile sees no streamingContent and mounts the editable inline editor (which refetches the persisted content). The synthetic streaming-file stays a pure preview.

* Revert "fix(mothership): swap to the inline editor once a file preview finishes streaming"

This reverts commit 25b12e4caa389109c2d5ed7f7d122e35d441f980.

* Revert "fix(mothership): use the inline markdown editor in the chat resource view"

This reverts commit 9430aa7fdc5050d002f2312d31dcf4a255e2de18.

* feat(files): rich markdown editor across files + chat, read-only for unsafe, robust load/save

- chat resource view streams into the rich editor (streamdown while streaming → editable on completion); agent persists server-side, editor never saves mid-stream
- round-trip-unsafe / >128KB markdown renders read-only in the rich editor (no Monaco, no corruption)
- markdown always uses the rich editor (dropped the inline-markdown opt-in flag)
- editor loads content as TipTap's initial content keyed by file id — strict-mode/SSR-safe, no content-sync effect
- fix autosave "Saving…" status suppression under React strict mode
- lock the streamed-file persistence handoff with a state-machine lifecycle test

* chore(files): remove dead code (unused FileViewer logger + EmbeddedWorkflowActions router)

* fix(files): derive markdown round-trip verdict from live content, not a locked stale snapshot

The gate locked isRoundTripSafe on the first post-stream snapshot, which is often the empty create_file buffer before the agent's server write lands — wrongly leaving an unsafe document editable. Derive the verdict from the current content (memoized on the bytes) so canEdit tracks the real payload.

* test(files): guard the rich editor dirty signal — open is never dirty, edits emit

* fix(files): lock the markdown round-trip verdict on opened content, never strand dirty edits

The round-trip-safety verdict now gates editability only at open time — computed once, on the exact
content the editor mounts with, and locked for its lifetime. A dirty document is round-trip-safe by
construction (the editor only emits safe markdown), so the verdict must never flip off mid-edit:
doing so disabled autosave, ⌘S, the toolbar Save and the unmount flush, stranding unsaved edits.
Locking on the opened (reconciled) content also fixes the stale post-stream empty-buffer snapshot,
and lets the redundant MarkdownFileEditor gate (plus its duplicate content fetch) be deleted.

* improvement(file-viewer): reuse shared copy hook, lazy frontmatter split

- code-block: replace hand-rolled copy-with-timeout with shared useCopyToClipboard
- rich-markdown-editor: compute frontmatter split once via lazy ref, drop redundant frontmatterRef
- round-trip-safety: correct stale comments (read-only, not raw editor fallback)

* feat(file-viewer): linked images, typed-link input rule, drag-to-reorder, churn fixes

- image: round-trip linked images/badges via an href attr + custom markdown tokenizer; make
  the image a drag handle so it can be grabbed and reordered
- link-input-rule: convert typed [text](url) to a link on the closing paren (normalized href)
- markdown-paste: render pasted markdown as rich content, guarded against code blocks
- round-trip-safety: behavioral link-count check replaces the static linked-image rejection
- extensions: trim the table serializer's blank lines to stop interior-table whitespace churn

* improvement(file-viewer): Backspace at start of a heading reverts it to a paragraph

Notion-style: ProseMirror's default joins or no-ops at a heading boundary, stranding the
heading style. A second Backspace then merges as usual.

* fix(file-viewer): don't upload pasted/dropped images into a read-only editor

handlePaste/handleDrop ran the workspace image upload without checking editability, so a
read-only doc (canEdit=false or a round-trip-unsafe file) could still trigger an upload.
Guard both on view.editable.

* fix(file-viewer): sanitize linked-image href; drop global leading-newline strip

- image: run the linked-image (badge) anchor target through normalizeLinkHref so a
  javascript:/data: href in a file can't execute on click; the markdown still preserves the
  raw target (file content unchanged)
- markdown-fidelity: the table serializer now trims its own surrounding blank lines, so the
  global leading-newline strip in postProcessSerializedMarkdown is redundant — removing it
  stops clobbering content that legitimately begins with whitespace

* feat(file-viewer): stream agent output directly into the rich editor; add more code languages

- rich-markdown-editor: the TipTap editor is now the only markdown surface. Agent output streams
  into it read-only (synced per chunk, autoscrolled), then the same instance hands off to an
  editable editor on settle — no separate streamdown preview, so no stream→edit flash. The
  round-trip verdict + frontmatter lock when the content settles.
- code-block/code-highlight/detect-language: register Go, Rust, Java, C, C++, C#, Ruby, PHP grammars
  and add detectors, so those blocks highlight and the picker offers them.
- css: style h5/h6 in the prose stylesheet.

* fix(sidebar): hydrate collapse state before paint to stop refresh flash

The collapsed sidebar swaps entire subtrees (collapsed flyout vs expanded
lists), but isCollapsed only resolved after the first paint via auto
rehydration, so a collapsed reload rendered the expanded tree into the 51px
rail and then reflowed — the misplaced/flashing content on refresh.

Adopt zustand's documented SSR pattern: skipHydration on the persist config
(first render keeps the default false, matching SSR HTML) and flush
persist.rehydrate() from a useLayoutEffect so the correct structure commits
in the same pre-paint frame. Removes the old race where onRehydrateStorage
lifted the data-sidebar-collapsed mask before React committed the rail.

* refactor(file-viewer): audit fixes — stale docs, DRY settle-lock, language detection

- rich-markdown-editor: rewrite the now-stale single-surface docstring (no PreviewPanel); extract a
  shared lockSettled() helper used by both the mount and stream-settle paths; guard the settle
  re-seed so it only setContent's when the body actually changed (no redundant doc rebuild)
- detect-language: stop misreading generics (List<String>) as HTML markup; detect Go type/struct
- code-block: export LANGUAGE_OPTIONS + add a test asserting every picker language has a registered
  Prism grammar (prevents picker/highlighter drift)

* refactor(file-viewer): remove dead markdown-preview renderer now superseded by the rich editor

Markdown files route exclusively to RichMarkdownEditor on both the read-only
and editable paths, so PreviewPanel's markdown branch and its Streamdown-based
renderer were unreachable. Delete MarkdownPreview and its renderers, callout/
frontmatter/checkbox machinery, and the now-unused remark/rehype/prism/streamdown
imports; drop the dead toggleMarkdownCheckbox/onCheckboxToggle plumbing in
text-editor. Keep the html/csv/svg/mermaid branches intact.

* refactor(file-viewer): drop dead streamingMode/append path, align naming, cover autosave

The streaming engine only ever runs in 'replace' mode (the only runtime callers
pass it); the 'append' branch of resolveStreamingEditorContent was unreachable.
Remove streamingMode + the StreamingMode type and thread it out of the 6
components that forwarded it — nextContent is now simply the streamed snapshot,
behavior-identical on the live path. Rename for codebase semantics: the boolean
prop streaming -> isStreaming, EditorKeymap -> RichMarkdownKeymap, the highlight
PluginKey KEY -> HIGHLIGHT_PLUGIN_KEY. Add a defensive isEditable guard to the
markdown paste handler (parity with the image handler; read-only must never
mutate). Add a dependency-free useAutosave test suite (debounce, min-display
window, no-data-loss when an edit lands mid-save, error/no-retry, Cmd+S flush,
streaming-disabled lock, unmount flush).

* fix(file-viewer): re-lock round-trip verdict + frontmatter on each stream settle

LoadedRichMarkdownEditor stays mounted across multiple agent edits to the same
file within a chat (previewContextKey is the chat id), but the settle effect only
locked settledRef when it was null — so a second stream into the same instance
kept editability and frontmatter tied to the first settled snapshot. A repeat
edit that is round-trip-unsafe would stay editable, and saves would re-attach the
stale frontmatter. Track wasStreaming and re-derive the verdict + frontmatter on
every stream->settle transition (user edits never re-derive, preserving the
don't-strand-edits rule). Verified red/green in the e2e streaming harness.

* test(file-viewer): lock link href sanitization for dangerous schemes from file content

Greptile flagged a possible javascript: link XSS. Verified TipTap 3.26.1 already
neutralizes javascript:/data:/vbscript: (and mixed-case/whitespace variants) from
file-loaded markdown to an empty href. Add a committed regression test that asserts
this against the real headless editor, so a future TipTap bump can't silently
reintroduce the issue.

* perf(file-viewer): cap the round-trip probe at 24KB and coalesce streaming syncs

@tiptap/markdown's parse is superlinear (~O(n2)) in document size — measured ~170ms
at 11KB, ~875ms at 23KB, multiple seconds past ~35KB — and it runs synchronously at
mount inside the round-trip-safety probe (twice) and the editor's own setContent. The
128KB cap allowed multi-second main-thread freezes; lower it to 24KB so the worst-case
mount stays near a second while still covering the vast majority of real markdown files
(larger files open read-only). Separately, coalesce streaming chunk-syncs to one re-parse
per animation frame so a fast-streaming agent doesn't re-parse the whole accumulating
doc per token. Typing latency was measured to be already excellent (sub-ms median, no
change needed); the only hot cost was the mount parse.

* perf(file-viewer): chunked markdown parsing to remove the O(n2) mount cost

@tiptap/markdown's whole-document setContent(md,'markdown') is superlinear in size,
freezing the main thread at mount for large files (~2.5s at 34KB, ~11s at 65KB) and
forcing a restrictive read-only cap. Parse block-by-block instead: a conservative
blank-line/fence-aware splitter (merges list/quote runs and indented continuations so
ambiguous structures stay atomic; reference-link/footnote/raw-HTML docs fall back to a
whole parse), each block parsed with the editor's own lexer via one reused headless
parser, assembled into a doc. This is linear and byte-identical to the one-shot parse —
measured ~15ms vs multiple seconds at 124KB+ — so the editor mount, streaming sync, and
round-trip probe are all linear, and the editable-size cap goes 24KB -> 256KB (covers
the p99 of real files). Fidelity + idempotency are pinned by unit tests, a 400-document
property/fuzz test, and adversarial edge cases (nested/loose lists, blockquotes, setext,
indented code, lazy continuation, HTML, reference links).

* fix(sidebar): render collapse state from a cookie so SSR matches

The server couldn't read localStorage, so a collapsed user's first paint
rendered the *expanded* tree at 51px — prefetched chat/workflow lists,
pinned-chat pin icons, and loading skeletons all crammed into the rail and
then reflowed once the store hydrated.

Mirror the collapse state into a sidebar_collapsed cookie (the shadcn/ui
sidebar pattern), read it in the workspace server layout, and seed the
sidebar's first render with it: structure is now correct on the server, so
the first paint is the real rail with no skeleton/pin/shift. The store
remains the post-hydration source of truth; the blocking script honors the
cookie for width when localStorage is absent so width and structure agree.

* refactor(sidebar): make the cookie the single source of truth for collapse

Consolidates the collapse machinery onto one source of truth instead of
layering the cookie on top of the legacy localStorage + CSS-mask system:

- Collapse persists only in the sidebar_collapsed cookie; the store seeds
  isCollapsed from it and drops it from localStorage (partialize + merge),
  removing the dual-write and the cross-tab desync it caused.
- Retire the redundant html[data-sidebar-collapsed] attribute + CSS mask now
  that the server emits the correct data-collapsed structure; also delete the
  dead sidebar-collapse-show/-remove/-btn rules.
- Blocking script reads the cookie for collapse (width stays in localStorage)
  and seeds the cookie once from the legacy flag so existing collapsed users
  keep their preference.
- Keep skipHydration + a pre-paint rehydrate for width only — the documented
  zustand SSR pattern, so _hasHydrated is deterministically false during SSR.

Width stays in localStorage; each field now has exactly one home.

* refactor(file-viewer): simplify + cleanup chunked-parse (linear merge, parse-once seed)

From the /simplify + /cleanup passes:
- splitMarkdownBlocks: build continuation runs and join each once instead of
  concatenating onto the growing previous block per group, which was O(n2) for a
  pathological single long loose list (now linear: 208KB loose list splits in ~3ms).
- rich-markdown-editor: seed the editor's initial content via a lazy useState
  initializer instead of useRef(parseMarkdownToDoc(...)), whose argument re-parsed the
  whole document on every render (i.e. every keystroke). Parses exactly once at mount.
- Document that the indent-merge rule is load-bearing for nested fenced code, and
  tighten the verbose inline comment blocks.

* refactor(sidebar): drop orphaned sidebar-collapse-btn class

Its CSS rule was removed with the data-sidebar-collapsed mask; the button's
collapse behavior is fully driven by the React isCollapsed ternary, leaving the
class name pointing at nothing.

* test(file-viewer): consolidate split test files into one per module

Match the dir's one-test-per-module convention: fold the markdown-parse property/fuzz
suite into markdown-parse.test.ts and the editability corpus into round-trip-safety.test.ts
(both already tested the same module from a separate-concern file). No coverage change —
same assertions, fewer files (12 -> 10).

* fix(file-viewer): make all editor controls respect read-only permissions

Every interactive control that calls updateAttributes/dispatches a command mutates
the doc even when read-only (ProseMirror commands run regardless of editable), so
gate them on editor.isEditable:
- bubble menu: the Cmd/Ctrl+K shortcut and shouldShow now bail when not editable, so
  a read-only doc can't open the link bar and setLink into it (Cursor finding).
- code block: the language picker renders as a static label when read-only (its
  onSelect mutates); copy + view-only wrap stay.
- image: no drag-to-reorder (draggable=false, no drag handle) and no resize handle
  when read-only; the image still renders and follows its link.
- links: a plain click now follows the link in read-only (reader) mode, while edit
  mode still requires a modifier so a plain click can place the cursor (Cursor finding).
Verified with new read-only permission e2e tests.

* fix(sidebar): honor collapsed cookie even when localStorage is corrupt

The blocking script read the collapse cookie inside the same try as
JSON.parse(localStorage); invalid persisted JSON fell through to the 248px
fallback and ignored a collapsed cookie, painting an expanded-width rail on
first load. Read collapse from the cookie first and parse the persisted width
in its own try so the two are independent.

* docs(sidebar): convert inline comments to TSDoc

* fix(file-viewer): resolve in-app workspace image URLs in the rich editor

The removed MarkdownPreview rewrote /workspace/{id}/files/{fileId} image src to the
serving endpoint /api/files/view/{fileId}; without it, in-app image URLs 404 in the
rich editor (Cursor finding). Re-add the rewrite as a display-only transform on the
rendered <img src> — the node's stored src attribute keeps the original path so markdown
round-trips unchanged. Absolute/non-workspace URLs pass through. Unit tested.

* fix(files): restore same-page anchor links in the rich markdown editor

Headings rendered by the TipTap editor had no slug ids (the old MarkdownPreview
got them from rehype-slug), so in-document table-of-contents links like
[section](#section) had no targets. Resolve the slug to its heading on click
(GitHub-style, duplicate-disambiguated) and scroll to it, with zero per-keystroke
cost.

* feat(files): render mermaid diagrams in the rich markdown editor

A code block renders as a Mermaid diagram when it is fenced ```mermaid or
auto-detected (an untagged fence whose first line opens with a diagram keyword,
the Linear/GitHub heuristic). Detection is display-only — the node stays an
ordinary code block and the markdown round-trips unchanged.

- Source while the caret is inside the block, diagram on blur; a Show source /
  Show diagram control plus copy, matching the code block's hover chrome.
- Clicking the diagram selects the node (same ring as an image), not flips source.
- Theme-aware (light/dark) via next-themes; the diagram frame shares the code
  block's chrome (one CSS source of truth).
- Extracted MermaidDiagram into a shared module so the editor reuses it without
  pulling preview-panel's heavy deps; rendered SVGs are memoized so toggling the
  source view and back is instant.

Covered by mermaid-diagram unit tests and the editor e2e harness.

* fix(files): harden the markdown editor (CRLF chunking, href allowlist, image escaping)

Final-audit follow-ups:
- splitMarkdownBlocks normalizes CRLF/CR first — a closing fence ending in \r
  no longer fails to match, which had collapsed Windows-authored files with
  fenced code into one block and defeated the linear chunker (perf regression).
- normalizeLinkHref rejects file://, blob:, and other non-network schemes
  (script/data schemes already rejected); network scheme:// (http/ftp/…) and
  bare host:port still pass.
- Image markdown serialization escapes alt/title delimiters and angle-brackets
  a src with spaces/parens, so they round-trip losslessly; linked-image anchors
  open in a new tab (target=_blank).
- Markdown paste routes through the chunker so a large pasted blob can't freeze
  the main thread.

* test(files): cover the code-highlight incremental re-tokenization gate

Export and unit-test changeTouchesCodeBlock: prose-only edits map decorations
(false), edits inside a code block or a setNodeMarkup language change re-tokenize
(true) — the perf-correctness path that keeps highlighting off the keystroke path.

* fix(files): keep relative links relative, navigate in-app links within the SPA

- normalizeLinkHref no longer prefixes `./`/`../` relative paths into `https://./…`
  (they round-trip and resolve correctly).
- Following a same-origin in-app link (e.g. /workspace/…) routes through the
  Next router (same tab) instead of always opening a new tab; modifier-click and
  external URLs still open a new tab.

* fix(files): linked images don't open a tab on a plain click in the editor

The linked-image anchor's native navigation was firing on a plain click in edit
mode (where handleClick intentionally returns false for caret placement). Prevent
the anchor's default so the editor's handleClick — gated on editable/modifier,
matching text links via openOnClick:false — is the sole navigator.

* fix(sidebar): match the collapse cookie value strictly (not a substring)

A substring search for 'sidebar_collapsed=1' also matched 'sidebar_collapsed=10',
desyncing the pre-paint sidebar rail and client store from the strict server read.
Parse the cookie value and compare it to '1' exactly, in both the pre-paint inline
script and readCollapsedCookie. Added a store test.

* fix(sidebar): reconcile migrated-legacy collapse before paint

A user whose collapse lived only in localStorage has no sidebar_collapsed cookie
at SSR (initialCollapsed=false), but the pre-paint script migrates them to a
cookie. The store's persist.rehydrate() is async (flips _hasHydrated after paint),
so the first paint showed expanded labels in the collapsed 51px rail. Reconcile to
the cookie synchronously in a useLayoutEffect (first render still matches the
server, so no hydration mismatch) — no narrow-rail flash.
2026-06-19 18:32:42 -07:00
Theodore Li 7349bf403f feat(files): password, email-OTP, and SSO auth for public file shares (#5140)
* feat(files): password, email-OTP, and SSO auth for public file shares

* fix(files): suppress filename in share previews for email/sso, not just password

* fix(files): normalize allow-list emails to lowercase; genericize shared SSO denial message

* fix(security): make isEmailAllowed case-insensitive; normalize email at client gates

* test(security): cover isEmailAllowed case-insensitive matching

* fix(security): bind auth cookie to auth type; password endpoint rejects non-password shares

* chore(db): format generated migration meta

* fix(files): share upsert validation returns 400 not 500; disabling always succeeds

* feat(access-control): org admins can restrict allowed file-share auth types
2026-06-19 18:53:12 -04:00
Siddharth Ganesan 5925651cbc feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits (#5138)
* feat(vfs): add lazy vfs + remove dynamic fields for prompt caching hits

* feat(vfs): send typed workspace snapshot for append-only deltas

Build the workspace inventory from the primary db (fixes replica-lag staleness)
and emit it as a typed VfsSnapshotV1 `vfs` payload alongside the markdown, so the
mothership can diff it into append-only baseline/delta messages. Generate the TS
contract mirror from the Go-owned JSON schema (sync-vfs-snapshot-contract) and
sort connector types so diffs stay byte-stable.

* fix(lint): fix lint

* fix(vfs): forward the typed snapshot through the branch payload builder

The branch buildPayload implementations hand-list the params they pass to
buildCopilotRequestPayload and forwarded workspaceContext but dropped vfs, so the
typed snapshot never reached the Go request (req.Vfs was always nil and the
append-only delta path never engaged). Forward vfs in both the workflow and
workspace branches, and add a regression guard asserting the branch threads it
through (the bug slipped past tests because post.test mocked the payload builder
and payload.test called it directly, bypassing the branch).

* improvement(contracts): update vfs contracts
2026-06-19 15:12:42 -07:00
Theodore Li 208d135dac feat(enrichment): add enrichment details sidebar with cost + provider cascade (#5139)
* feat(enrichment): add enrichment details sidebar with cost + provider cascade

* fix(enrichment): address review — persist detail on cancel/skip, exclude not_run from ran count, refetch on panel open

* fix(enrichment): keep cascade detail sticky on upsert; mark unattempted providers not_run on abort

* fix(enrichment): show Cancelled in details panel for aborted runs
2026-06-19 17:19:13 -04:00
Theodore Li 9d2a6ef043 feat(logs): redact PII from workflow logs via configurable rules (#5136)
* feat(logs): redact PII from workflow logs via configurable rules

Enterprise PII redaction for workflow execution logs, configured under
Data Retention as org-scoped rules (each rule picks entity types + which
workspaces it applies to). Reuses the guardrails Presidio engine in mask
mode at the log-persist choke point, with a check-digit-validated VIN
recognizer. Also adds per-workspace data-retention-hours overrides.

* fix(logs): widen PII entity visibleValues to string[] for strict build typecheck

* fix(logs): redact error/trigger/executionState; keep guardrails import lazy

- Extend PII redaction to span error/errorMessage/toolCalls and top-level
  error/completionFailure/trigger/executionState (Bugbot: PII in execution
  metadata). executionState is safe to redact — resume reads from the separate
  pausedExecutions table, not the log copy.
- Lazy-import validate_pii in pii-redaction so the Python/child_process
  guardrails module stays out of the static middleware/RSC graph.
- Type the org retention mutation to the contract body (optional, non-null).

* refactor(logs): drop per-workspace retention override; PII redaction stays org-scoped

- Remove the unused per-workspace data-retention-hours override (no UI; superseded
  by workspace-scoped PII rules). Reverts cleanup-dispatcher to org-only retention,
  drops resolveEffectiveRetentionHours, the workspace.dataRetentionSettings column +
  migration, and the workspace data-retention route/contract/hooks. Fixes Bugbot's
  null-as-unset finding by removing the buggy path entirely; org retention behavior
  is unchanged.
- Stop re-checking isWorkspaceOnEnterprisePlan at persist time (it returns false on
  transient errors, which would fail-open and leak PII). Enabled rules already imply
  entitlement; redact whenever rules apply (fail-safe).

* fix(logs): redact oversized strings and executionData.environment

- Drop the per-string size cap in PII redaction: oversized strings were left
  unmasked (leak). Nothing is skipped now; large payloads still fail-safe via the
  total-bytes ceiling + per-chunk timeout (scrub, never leak).
- Add executionData.environment (incl. variables) to the redaction set.

* refactor(logs): single-scope PII rules with most-specific-wins resolution

Each rule now targets one scope — all workspaces (workspaceId: null) or a single
workspace — with workspaceId unique across rules. Resolution is most-specific-wins
(a workspace's own rule overrides the all rule), not union; an empty specific rule
exempts that workspace. Matches Access Control's resolveWorkspaceGroup precedence.
UI 'Applies to' becomes a single-select; Add rule disables when all scopes are taken.

* feat(logs): default + workspace-overrides UI for PII redaction

Reshape the PII redaction settings into a 'Default (all workspaces)' block plus a
'Workspace overrides' list, making the most-specific-wins precedence explicit
(overrides replace the default; unlisted workspaces use it). Same data model
(workspaceId null = default), UI only.

* improvement(logs): clearer default/overrides PII UI

Drop the uppercase section labels and the overrides description; gate the
Workspace overrides section behind a configured default; use a single Delete
action; 'Add redaction' creates the all-workspaces default and disappears once set.

* fix(guardrails): handle stdin EPIPE in PII python spawns

Attach an 'error' listener to the child's stdin in both runPythonScript (the
batch masking hot path) and executePythonPIIDetection. A 256KB chunk can exceed
the OS pipe buffer, so if the Python process exits mid-read (OOM/kill) the EPIPE
emitted on stdin was unhandled and would crash the Node process. Funnel it into
the promise rejection so the fail-safe scrub path handles it gracefully.

* fix(logs): redact executionData.correlation

The top-level correlation field is copied from pre-redaction trigger data, so
webhook/schedule correlation values could persist unredacted. Add it to the
redaction set alongside trigger/environment.

* fix(logs): enforce unique PII rule scope server-side

The contract accepted multiple rules with the same workspaceId (or several
null all-rules); resolution is first-match, so duplicates could disagree with
the UI. Add a schema refine rejecting duplicate scopes.

* fix(logs): re-hydrate data-retention form on org switch

The form hydrated once via a boolean ref, so switching the active org left stale
retention days + PII rules and saves targeted the new org with old config. Key
hydration on orgId so it re-loads per org.
2026-06-19 17:15:46 -04:00
Vikhyath Mondreti 13b5d215e9 improvement(access-controls): docs, terminology, fix delete bug (#5141)
* improvement(access-controls): dedup independent block names

* improvement(access-controls): fix delete button, naming in perm group modal
2026-06-19 13:48:07 -07:00
Vikhyath Mondreti 91f9dfdaec improvement(governance): derived access (#5134)
* improvement(governance): org-ws-credential roles clarity

* revert isHosted

* improvement(credentials): code cleanup

* address comments

* make kb cascade delete on user hard delete

* revert env flags

* chore(db): drop local 0242 migration to regenerate after merging staging

Our 0242 collides with staging's 0242. Remove it (and its snapshot +
journal entry) so the KB-cascade migration can be regenerated with the
correct number on top of the merged staging migrations.

* chore(db): regenerate kb→workspace cascade migration as 0243

Regenerated via drizzle-kit generate on top of the merged staging
migrations (staging took 0242). Re-applied the safety edits: NOT VALID
+ separate VALIDATE on the FK re-add, and the -- migration-safe note on
the DROP. check:migrations passes.

* improve copy

* update docs
2026-06-19 12:47:09 -07:00
Theodore LiandClaude Opus 4.8 c419a34317 feat(tables): raise per-plan table limits (free 5/50k, pro 100/100k, max 1k/500k) (#5135)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:05:19 -04:00
Theodore Li f0b3550729 feat(files): public share links for workspace files (#5130)
* feat(files): public share links for workspace files

* improvement(files): drop reserved public_share columns until used; sync audit mock

* fix(files): share modal tracks authoritative saved state until toggled

* feat(files): per-IP rate limit on public share endpoints

* fix(files): address PR review — public CSV OOM, content cache, share FK, soft-delete filter, download anchor

* fix(files): disable CSV import action in read-only preview (public share)

* refactor(files): drive CSV preview import affordance off readOnly, not disableImport

* fix(files): version public viewer caches by file updatedAt so edits aren't stale

* fix(files): 409 (not corrupt source) when a shared generated doc has no compiled artifact

* feat(files): gate public sharing behind an access-control permission
2026-06-18 21:26:49 -04:00
Vikhyath Mondreti 267e49c69c improvement(workspaces): auto-add without invite if part of organization (#5132)
* feat(workspaces): auto-add without invite if part of organization

* reverse feature flag hardcoding

* address comments

* improve ux for org invite modal
2026-06-18 15:48:02 -07:00
Theodore LiandClaude Fable 5 63fdc472c1 improvement(block): table empty-state filter/sort builders + upsert conflict-column selection (#5123)
* ci(migrations): fail dev schema push with an actionable error on rename/drop prompt

`drizzle-kit push --force` only suppresses the data-loss confirm, not the
rename-vs-drop disambiguation prompt. That prompt fires whenever a diff both
adds and drops tables/columns at once (e.g. migration 0231 created
sim_trigger_state while dropping the workspace_notification_* tables), and in
CI it crashes with a bare "Interactive prompts require a TTY" stack trace.

Catch that specific failure in the dev push step and emit a GitHub error
annotation explaining the cause and the fix (drop the stale objects on the dev
DB to match schema.ts — the same DROPs the versioned migration already applied
to staging/prod), instead of leaving an opaque trace. Exit status is preserved
either way.

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

* improvement(tables): empty-state filter/sort builders + upsert conflict-column selection

* improvement(tables): throw on ambiguous upsert instead of guessing the conflict column

* Revert "ci(migrations): fail dev schema push with an actionable error on rename/drop prompt"

This reverts commit 2626482269.

* improvement(tables): unique-column picker for upsert + richer get-schema (counts, ids, live plan row limit)

* fix(tables): honor OR boundary when skipping incomplete filter rows

* fix(tables): source workspaceId for column selector from route context

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-18 18:15:12 -04:00
Vikhyath Mondreti 58312a10e5 improvement(misc): add more sportmonks tools, improvestreaming ux (#5129) 2026-06-18 12:31:54 -07:00
Siddharth Ganesan ea83be3bbe fix(mship): add folder rename tools and locked workflow status (#5126)
* fix(mship): add folder rename tools and locked workflow status

* fix(mship): manage_folder bug fixes

* improvement(mship): clean up deprecated fields from contracts

* test(mship): update tool-call display title tests for client-derived titles

Display titles now come from the Sim-side name resolver, not the stream's
ui.title/phaseLabel. Update the read lifecycle test to expect the
name-derived title and drop the obsolete phaseLabel-fallback test.

* fix(contracts): lint
2026-06-18 11:58:38 -07:00
Siddharth GanesanandVikhyath Mondreti e5f3965ed1 feat(mship): add parallel subagents, improve streaming performance (#5122)
* feat(subagents): add support for parallel subagents

* fix(subagents): address parallel-subagent bugs

* progress on streaming refactor

* improvement(subagents): update comment to reflect new go feature flag

* debug mode progress

* remove debug logs

* fix(validation): add escape annotation

* improvement(code): remove dead fallbacks

* fix subagent lane fallback issue

* fix(mothership): increase default redis event limit to 100k from 5k

* fix(mothership): streaming invariant projection enforcement

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-06-18 11:27:49 -07:00
Theodore Li 597d7eafb5 fix(tables): enforce row limits against the current plan, not a frozen per-table cap (#5120)
* fix(tables): enforce row limits against the current plan, not a frozen per-table cap

* fix(tables): gate multi-batch CSV create + initial rows against the plan, harden limits cache bound

* fix(tables): thread running row count through copilot batchInsertAll capacity check

* chore(tables): align tx-variant capacity docstrings

* fix(tables): map row-limit errors to 400 in create-from-CSV import

* feat(tables): add Upgrade action to the row-limit toast

* fix(tables): keep CreateTableData.maxRows so staging callers type-check after merge

* improvement(tables): route row-limit Upgrade action to the explore-plans page
2026-06-17 22:38:55 -04:00
Theodore Li badfbc3bdf fix(resource): left-align table filter/sort when there's no search (#5128)
The unconditional ml-auto from #5117 right-aligned the embedded table
editor's filter/sort cluster, which has no search bar. Only push the
aside + filter/sort group right when a search occupies the left; without
a search it stays left-aligned as before.
2026-06-17 22:26:37 -04:00
Theodore Li 63a3e6d2cb feat(files): stream large CSV previews and add import-as-table (#5125)
* feat(files): stream large CSV previews and add import-as-table

* fix(files): validate fileId in csv-preview route, guard double-import, fix sniff perf and toggle flash

* fix(files): scope mothership preview-toggle loading guard to CSV files only
2026-06-17 21:54:18 -04:00
Theodore LiandClaude Fable 5 a028d07e7b improvement(mothership): user_table speed parity — limit bounds, background import/delete/update jobs (#5012)
* improvement(mothership): user_table speed parity — limit bounds, async import/delete/update jobs

- query_rows / filter ops clamp limit to the contract maxes; query_rows
  skips execution metadata.
- import_file / create_from_file (large CSV/TSV) and delete_rows_by_filter
  (>1000 unbounded matches) dispatch background table jobs, claiming the
  per-table job slot; inline paths claim the slot too.
- update_rows_by_filter now escalates the same way: >1000 unbounded matches
  run as a background table job (new 'update' job type + runTableUpdate worker
  + tableUpdateTask), so a broad update on a huge table no longer loads every
  row into the request. Best-effort/non-atomic and skips workflow recompute
  (documented); unique-column patches stay inline. Pagination is limit/offset.

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

* docs(mothership): trim user_table catalog copy to the essentials

Drop the verbose doomedCount/affectedCount, delete-mask, workflow-recompute,
and unique-column asides from the bulk-op descriptions. The model only needs:
large ops return { jobId }, limit maxes at 1000, one job per table.

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

* improvement(mothership): make user_table limit cap internal, not model-facing

The model can now pass any limit — no "cannot exceed 1000" rejection. 1000
becomes an internal threshold: query_rows clamps the page to MAX_QUERY_LIMIT
(totalCount signals truncation; the model pages with offset), and bulk filter
ops above the cap run as background jobs.

update_rows_by_filter loads full row data inline, so an explicit limit above
the cap escalates to the background worker with a new maxRows budget (the worker
stops after maxRows; update has no read mask so the cap is exact). delete only
loads ids inline, so an explicit limit (any size) stays inline — only unbounded
deletes use the masked background path, which would over-hide a bounded delete.

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

* improvement(mothership): bounded delete above the cap runs async, not inline

An explicit delete limit now mirrors update: ≤1000 runs inline, above the cap it
escalates to the background worker honoring the limit via maxRows — instead of
always staying inline. The worker stops after maxRows (per-page fetch capped to
the remaining budget).

Bounded background deletes skip pendingDeleteMask: the filter-based mask hides
every match, which would over-hide the rows beyond the cap the job never deletes.
Unmasked, a bounded delete is eventually consistent like a bounded update (rows
disappear as deleted), and doomedCount is omitted from the payload so the count
isn't double-subtracted.

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

* docs(mothership): tidy user_table limit/offset param copy

Drop "Any value is allowed" from the limit description and restore the original
offset description.

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

* fix(tables): skip pendingDeleteMask for bounded background deletes

The bounded-delete commit (f1ee3e9) persisted maxRows and omitted doomedCount
but the pendingDeleteMask guard that makes it work was left uncommitted, so the
shipped mask still hid every filter+cutoff match — over-hiding the rows beyond
maxRows that the job never deletes (they vanished from reads until the job ended,
then reappeared). Return no mask when maxRows is set: a bounded delete is
eventually consistent (rows disappear as deleted), like a bounded update.

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

* docs(mothership): drop redundant background note from limit arg

The op descriptions already cover background escalation; the limit arg only
needs to say what the param does.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-17 19:34:39 -04:00
Waleed 7d46103d09 chore(deps): remove unused dependencies and harden CI supply chain (#5119)
* chore(deps): remove unused dependencies and harden CI supply chain

Dependency cleanup:
- Remove unused deps: papaparse, unified, and 6 unused Radix primitives
  (alert-dialog, radio-group, scroll-area, separator, toggle, visually-hidden)
  plus @tanstack/react-query-devtools (all verified zero imports repo-wide)
- Consolidate jwt-decode into the existing jose dependency (decodeJwt)
- Migrate react-window to @tanstack/react-virtual to drop a redundant
  virtualization library (terminal, structured-output, code viewer)
- Remove the better-auth-harmony plugin and its gating env flag

Supply-chain hardening:
- SHA-pin every GitHub Action to a full commit SHA with a version comment
- Pin CI bun-version to 1.3.13 (was "latest" in the release job)
- Raise bun minimumReleaseAge cooldown from 3 to 7 days
- Add a non-blocking `bun audit` step in CI
- Add a CODEOWNERS gate routing dependency-manifest changes to @simstudioai/deps

* chore(deps): remove unused apps/docs dependencies (@tabler/icons-react, dotenv-cli)

* style(search-modal): use Send icon for Invite teammates action

* feat(search-modal): surface New chat as the top action above Create workflow

* feat(search-modal): add Secrets to the pages list
2026-06-17 14:56:43 -07:00
Theodore Li 08bcacd74e fix(copilot): mount input tables with display-name CSV headers, not column IDs (#5121) 2026-06-17 17:45:47 -04:00
Waleed fcfa41cd42 fix(azure): replace Azure DevOps icon with Azure icon and remove AzureDevOpsIcon (#5118) 2026-06-17 13:37:23 -07:00
Waleed cae1769117 improvement(knowledge): align connected-sources rows and move source chip left of filter/sort (#5117)
* improvement(knowledge): align connected-sources rows and move source chip left of filter/sort

- Drop the -mx-2 on the connectors list so rows respect the ChipModalBody
  gutter: the row hover no longer bleeds to the modal edges and row content
  lines up with the px-4 header.
- Add a 'leading' slot to ResourceOptions (left of the filter/sort cluster) and
  render the knowledge connected-source chip there instead of the far-right
  'aside', so it reads as part of the control row. 'aside' stays right-aligned
  for the table editor's run/stop control.

* improvement(resource): render options aside left of filter/sort

The options-bar aside has a single other consumer (the table editor's embedded
run/stop control), so instead of adding a separate slot, render aside itself to
the left of the filter/sort cluster. Drops the extra slot and keeps one
canonical control position; the run/stop control moves left too, which is fine
for a status widget.

* fix(resource): keep options aside grouped with filter/sort without a search bar

Group aside + the filter/sort cluster in one ml-auto right-aligned container
instead of relying on the search's flex-1 to anchor them. Without this, an
options bar with no search (the embedded mothership table editor) split aside
to the far left and filter/sort to the far right via justify-between.

* docs(resource): clarify aside groups with filter/sort regardless of search
2026-06-17 13:28:00 -07:00
Waleed 4d39b0cbf8 feat(connectors): use resource selectors for KB connector config (#5116)
* feat(connectors): use resource selectors for KB connector config

Replace raw ID text inputs with selector pickers (canonical selector +
manual-input pairs) across Google Drive/Docs/Forms/Sheets, Notion, Monday,
and Webflow KB connectors, so users pick folders/spreadsheets/pages/boards/
collections instead of pasting IDs — matching the workflow blocks.

- Add multi-select where the sync handler supports it (Drive/Docs/Forms
  folders, Monday boards, Webflow collections) via parseMultiValue
- Add shared escapeDriveQueryValue/buildDriveParentsClause helpers for safe
  multi-folder Drive queries
- Add ConnectorConfigField.mimeType, plumbed into the selector context
- Fix Webflow listingCapped not set on maxItems truncation (deletion-
  reconciliation data-loss safety)

Fully backward compatible: legacy single-string IDs and CSV both normalize
via parseMultiValue; resolved canonical keys are unchanged.

* fix(webflow): set listingCapped on within-page maxItems truncation

When a collection's items fit in a single API page but maxItems cuts the
list within that page, neither hasMoreInCollection nor hasMoreCollections is
true, so listingCapped was not set and the sync engine could hard-delete
still-existing documents. Add the within-page drop signal to the guard.
2026-06-17 12:56:46 -07:00
Theodore Li ea505f0388 improvement(tables): versioned CSV snapshot cache for table mounts + parallel multipart uploader (#5108)
* improvement(tables): versioned CSV snapshot cache for table mounts + parallel multipart uploader

* chore(db): drop colliding 0239 migration (renumber pending)

* chore(db): renumber rows_version migration to 0240 (off staging's 0239)

* improvement(tables): mount snapshots by presigned URL so the sandbox fetches directly (raise cap to 500MB)

* fix(tables): allow url sandbox entries in the function-execute contract; key snapshot by column shape so schema edits invalidate it

* chore(e2b): log sandbox inputs split by url-fetch vs inline write

* improvement(tables): order export + snapshot rows by order_key so the CSV matches the grid under fractional ordering
2026-06-17 15:26:05 -04:00
Waleed c907b1194b improvement(supabase): add Edge Functions tool; correct storage output shapes + harden tools (#5112)
* improvement(supabase): add Edge Functions tool; correct storage output shapes + harden tools

- Add supabase_invoke_function tool (POST/GET/PUT/PATCH/DELETE /functions/v1/{name})
- upsert: support on_conflict; storage upload: support cache-control
- Fix storage copy/move/upload/delete-bucket output properties to match live API
- get_public_url: build URL via directExecution (no spurious network call)
- text_search: validate column identifier
- Strip non-TSDoc section-label comments

* fix(supabase): harden rpc/text_search identifiers; drop unused get_public_url apiKey

- rpc: validate + encode functionName (SSRF/injection parity with vector_search)
- text_search: validate language config interpolated into the PostgREST operator
- get_public_url: remove unused apiKey param + dead auth headers (public endpoint needs no auth)
- create_bucket: tighten output description to match the {name}-only response

* improvement(tavily): mark optional params advanced; fix empty content output

- Mark 29 optional search/extract/crawl/map subBlocks as mode: advanced (keep query/urls/url/apiKey basic)
- Fix search transformResponse: populate content from result.content (was result.snippet, always empty)
- Guard data.results with ?? []; correct country placeholder to a lowercase name
- Rewrite stale TavilySearch/Extract response interfaces; drop dead duplicate interfaces

* fix(supabase): valid Cache-Control directive on upload; clarify functionName description

- storage upload: expand a bare numeric cache-control to `max-age=<n>` (a raw number is not a valid Cache-Control header)
- block: functionName input description now covers RPC, vector search, and Edge Function invoke

* fix(supabase): edge-function error handling + reject array headers

- invoke_function: drop unreachable !response.ok branch (executor throws on non-OK before transformResponse runs and surfaces the error body); document the success-only contract
- invoke_function: ignore non-object (array) headers so JSON arrays can't produce numeric-index header names
- block: reject array/non-object Edge Function headers with a clear error in config.params

* fix(supabase): scope Edge Function method/body/headers to invoke_function

Prevents a stale `method` value (e.g. from the Edge Function field) from leaking
into other operations' params. The tool executor lets `params.method` override a
tool's static verb (tools/utils.ts), so an unscoped value could turn a read into
DELETE/POST against PostgREST. Now method/body/headers are only passed for the
invoke_function operation. Adds a block-level regression test.

* fix(supabase): only parse Edge Function body/headers for invoke_function

Stale or invalid functionBody/functionHeaders left in the block (common when
switching operations) were parsed and validated for every operation, so they
could throw before unrelated tools ran even while hidden. Moved parsing and
validation inside the invoke_function guard; added a regression test.

* fix(supabase): include last_accessed_at as a storage list sort option

The Storage list API accepts last_accessed_at for sortBy; add it to the tool
description and the block dropdown so the surfaced options match the API.
2026-06-17 12:23:55 -07:00
Waleed 11e23131fe feat(google): Maps Pollen/Solar, Custom Search expansion, and live-API fixes across Google integrations (#5113)
* feat(google): add Maps Pollen/Solar, expand Custom Search, fix Ads/Groups/Contacts/Slides

New capability:
- Google Maps: add Pollen Forecast and Solar Potential tools (API-key, google_cloud BYOK)
- Google Custom Search: add start/dateRestrict/fileType/safe/searchType/siteSearch/
  siteSearchFilter/lr/gl/sort params, htmlTitle/htmlSnippet/formattedUrl/mime/fileFormat/
  cacheId/image result fields, and nextPageStartIndex pagination

Fixes (validated against live API docs):
- Google Ads: bump all tools from sunset v19 to v24
- Google Groups: forward OAuth credential under oauthCredential (was dropping token in 11
  ops), forward all update_settings fields, JSON.stringify update_settings/add_alias bodies
- Google Contacts: include required metadata.sources[].etag in updateContact body (fixed 400)
- Google Slides: remove unsupported GIF thumbnail mimeType (API only allows PNG)
- Google Sheets: wire delete_rows/delete_sheet/delete_spreadsheet into the V2 block
- Google Custom Search: throw on API error responses instead of returning empty success;
  num optional + Number-coerced; pagemap typed unknown

* docs(google): regenerate integration docs for new and updated operations

* fix(google_maps): correct Solar requiredQuality enum to BASE

The Solar API ImageryQuality enum is HIGH/MEDIUM/BASE (+ UNSPECIFIED) per the
live docs; there is no LOW. Selecting "Low" sent requiredQuality=LOW which the
API rejects as INVALID_ARGUMENT, and the valid BASE tier was unreachable.
Replace LOW with BASE in the tool param/output descriptions, the type union,
and the block dropdown.

* fix(google_maps): guard !response.ok in Pollen/Solar; use ?? for color channels

Address Greptile review:
- Pollen and Solar transformResponse now check !response.ok || data.error
  (matches the Custom Search fix); a gateway error without an error key in the
  body no longer returns empty/zeroed output silently.
- Pollen color channels use ?? instead of || so a legitimate 0 isn't treated
  as missing (consistent with the other numeric fields in the file).

* fix(google_maps): guard against NaN days in Pollen forecast

Address Cursor Bugbot: a non-numeric `days` input parsed to NaN and was
forwarded as `days=NaN` (the tool's `?? 1` only catches undefined, not NaN),
breaking the forecast call. The block now coerces invalid input to undefined,
and the tool defaults to 1 unless `days` is a finite number.

* fix(google): clamp Pollen days to 1-5; stop forwarding stale group settings fields

Address Cursor Bugbot:
- Pollen: clamp days to the documented 1-5 range (truncating fractionals) so 0,
  negatives, or >5 can't be sent to the API.
- Google Groups update_settings: the block has no dedicated settings subblocks,
  so forwarding name/description from params could leak stale values from
  create_group/update_group and unintentionally rename the group. Forward only
  oauthCredential + groupEmail from the block (the tool's own param schema still
  exposes the settings fields for the agent path).

* fix(google_sheets): fail fast on non-numeric delete indices

Address Cursor Bugbot: delete_sheet/delete_rows parsed deleteSheetId/startIndex/
endIndex with Number.parseInt but didn't validate, so non-numeric UI input became
NaN and was forwarded (the v2 delete tools only reject null/undefined), breaking
the batchUpdate. The block now throws a clear error when any of these is not a
valid number.

* fix(google_search): clamp num to 1-10 and normalize start

Address Cursor Bugbot: num was coerced with Number() but not bounded, so values
like 11 or fractionals reached the API and failed. The tool now truncates and
clamps num to the documented 1-10 range and only sends a positive integer start,
ignoring non-numeric/out-of-range input.
2026-06-17 12:15:48 -07:00
Waleed d7fd0405f9 improvement(search): align cmd+k action icons + highlight with the design system (#5114)
* improvement(search): align cmd+k action icons + highlight with the design system

- Each Actions verb now uses the exact icon from its real location: Fit to view
  -> Scan (workflow-controls), Copy workflow link -> Duplicate (nav context menu),
  Invite teammates -> User (settings teammates nav). Run/Create/Import already matched.
- Remove the Toggle theme action and its now-dead useTheme wiring.
- Matched-text highlight now uses the design-system search tokens
  (--highlight-match-bg / --highlight-match-text), matching the SearchHighlight
  component used in knowledge-base and code search, instead of an ad-hoc font-semibold.

* improvement(search): use font-medium for matched-text emphasis in cmd+k

Drop the colored background highlight in favor of the design system's standard
emphasis weight (font-medium, used by Button/Label/Input/Table). Lighter than
the previous semibold and avoids a background, keeping the palette's clean,
undecorated text style.
2026-06-17 11:52:57 -07:00
Waleed 9e9f2b9e1f fix(realtime): debounce the reconnecting toast to stop transient-blip flashes (#5111)
* fix(realtime): debounce the reconnecting toast to stop transient-blip flashes

The "Reconnecting..." persistent toast fired the instant isReconnecting
flipped true, so sub-second transport blips that self-heal on the first
retry flashed a scary alert. Add useStableFlag, an anti-flicker boolean
that delays the rising edge (2s, so brief blips never surface) and holds
the falling edge (1.5s min visible, so a drop just past the delay does not
flash-and-vanish). The socket flag stays accurate; only the user-facing
alarm is smoothed. State machine extracted into a framework-agnostic
controller with unit coverage for both flicker modes.

* fix(realtime): reset stable-flag React state on options change; de-vacuous blip test

Address Greptile review:
- useStableFlag: reset React state to the fresh controller's baseline when
  the controller is recreated on an options change, so a dynamic consumer
  changing delayMs/minVisibleMs while active with value already false can no
  longer strand the flag at true.
- test: read the live probe.active getter in the blip test instead of a
  destructured snapshot, which was bound to false at destructure time and
  made the assertion vacuous.
2026-06-17 11:12:41 -07:00
Waleed 05cd7d92a8 feat(search): actions, fuzzy matching, and highlighting in cmd+k palette (#5110)
* feat(search): actions, fuzzy matching, and highlighting in cmd+k palette

Add a context-aware actions layer to the cmd+k search palette (Run workflow,
Create workflow/folder, Import workflow, Fit to view, Copy link, Invite
teammates, Toggle theme), replace the substring matcher with a boundary-anchored
fuzzy matcher (initialisms, typos, multi-word) that is a strict superset of the
old behavior, highlight matched characters, and rank against clean human text
instead of structural id/uuid tokens. Expose invoke() on the global commands
provider so the palette runs real registered commands.

* fix(search): highlight the matched substring, not an earlier scattered occurrence

Contiguous substring matches (exact/prefix/contains) now report the substring's
own indices instead of the greedy subsequence scan positions, so HighlightedText
bolds the characters the user actually matched. Restructures fuzzyMatch to handle
the substring tier first; scores are unchanged for these cases.

* fix(search): log clipboard copy failures and make fuzzy positions read-only

- Copy workflow link now logs on clipboard write failure instead of silently
  swallowing the error, matching the sidebar's copy-link convention.
- FuzzyResult.positions is now readonly and the NO_MATCH singleton's array is
  frozen, so the shared instance can never be mutated by a caller.
2026-06-17 10:47:17 -07:00
Waleed 8b93e43037 improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs (#5109)
* improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs

- BigQuery: mark null-defaulted outputs optional (get_table type/numRows/numBytes/creationTime/lastModifiedTime/location, list_datasets location, list_tables type, query totalBytesProcessed)
- Google Forms: add response pagination (pageToken + filter params, nextPageToken output), fix pageSize visibility, advanced-mode pagination subBlocks + filter wandConfig
- PageSpeed: add a 7th BlockMeta template (competitor benchmark)
- Regenerate integration docs; add manual intro sections to new datagma/dropcontact/enrow/icypeas/leadmagic pages

* fix(docs-gen): preserve apostrophes in tool descriptions when generating docs

The doc generator extracted tool descriptions with a character class that
excluded both quote types (['"]([^'"]...)['"]), so a double-quoted description
containing an apostrophe (e.g. "Find someone's email") was truncated at the
apostrophe — the generated docs/catalog showed stubs like "Find someone".

Anchor extraction on the actual opening quote (single/double/backtick), matching
the existing extractDescription helper, in both buildToolDescriptionMap and
extractToolInfo. Regenerated docs restore full descriptions across all affected
integrations (Apollo, Ahrefs, LeadMagic, Findymail, OpenAI, Slack, etc.).

* fix(docs-gen): resolve tools defined in a sibling file + scope params per tool

The doc generator located a tool's definition only by filename convention
(decompress.ts / index.ts), so file_decompress — which lives in compress.ts
alongside file_compress — fell back to index.ts and rendered an empty Input
table. It also read the params block from the first tool in a multi-tool file,
so every tool in such a file inherited the first tool's inputs/outputs.

- getToolInfo: when no candidate file declares the exact tool ID, scan the whole
  tool-prefix directory for the file that does.
- extractToolInfo: read the params block scoped to the specific tool, falling
  back to the full file for tools that inherit params via spread.

Regenerated docs eliminate ~50 empty/incorrect input tables across integrations
(clickhouse, rb2b, reddit, file, etc.); param-less OAuth-only tools correctly
keep an empty input table.
2026-06-16 23:05:30 -07:00
Waleed 80735b424b fix(locks): enforce workflow/folder locks on the agent + close manual-UI create gaps (#5107)
* fix(locks): enforce workflow/folder locks on the agent + close manual-UI create gaps

The copilot/agent workflow & folder mutation tools and the edit_workflow
tool bypassed lock enforcement, so the agent could edit a locked workflow
and move/create workflows into a locked folder. Add assertWorkflowMutable/
assertFolderMutable guards (from @sim/workflow-authz) to every agent
mutation path, mirroring the REST API.

Also close two parity gaps on the manual-UI REST side: creating a workflow
into a locked folder and creating a subfolder under a locked parent were
previously unguarded. The realtime collaborative canvas already enforced
workflow-level locks server-side.

* fix(locks): normalize optional folderId to null for assertFolderMutable

* refactor(locks): hoist constant folder-lock check out of move loop; scope test mocks with Once

* refactor(locks): drop redundant ensureWorkflowAccess fetch in rename
2026-06-16 20:28:35 -07:00
Waleed 83531452e5 fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons (#5104)
* fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons

On a cold load (e.g. when the browser discards an idle tab and reloads),
the persistent sidebar started with an empty React Query cache and
client-fetched its chat + workflow lists, flashing loading skeletons.

Prefetch both lists server-side in the workspace layout and hydrate them
via HydrationBoundary, under the same query keys and mappers the client
hooks use, so the sidebar paints populated on the first render. The
prefetch runs concurrently with the existing org-settings fetch and
never throws, so it adds no blocking work in the common case and falls
back to client fetching on error.

* refactor(prefetch): call data layer directly instead of internal HTTP self-fetch

The sidebar and settings prefetches fetched their data by making internal
HTTP requests to our own API routes. Replace those self-fetches with direct
calls to shared server-side data functions, so each route handler and its
prefetch read from one source with no extra network hop, serialization, or
re-auth.

- Extract listWorkflowsForUser (lib/workflows/queries) and listMothershipChats
  (lib/copilot/chat) from their routes; both routes and the sidebar prefetch
  now call them.
- Extract getUserSettings/getUserProfile (lib/users/queries) shared by the
  settings/profile routes and their prefetches.
- Subscription prefetch calls the existing getSimplifiedBillingSummary +
  getEffectiveBillingStatus directly.
- Sidebar prefetch checks workspace access once via checkWorkspaceAccess and
  skips silently when denied.

* refactor(prefetch): share mothership chat list staleTime constant

Export MOTHERSHIP_CHAT_LIST_STALE_TIME from the chats hook and use it in both
useMothershipChats and the sidebar prefetch, mirroring WORKFLOW_LIST_STALE_TIME
so the prefetch and client hook can't drift.

* fix(prefetch): keep subscription prefetch on the wire shape via internal billing API

The billing summary returns Date fields (and an untyped metadata blob) that the
JSON API serializes to strings. Calling the data layer directly would cache Date
objects (App Router preserves them through RSC serialization), mismatching the
string wire shape the client useSubscriptionData hook caches. Route the
subscription prefetch through the internal billing API so server-hydrated and
client-fetched data share the exact same shape. The date-free general-settings
and profile prefetches keep calling the data layer directly.
2026-06-16 19:28:51 -07:00
Waleed a82b44d36d perf(db): logs-list index, drop redundant indexes, replica routing, hot-path write cleanups (#5105)
* perf(db): logs-list index, drop redundant indexes, replica routing, hot-path write cleanups

* fix(logs): keep /api/v1/logs on primary db — its permissions join is the auth gate, not replica-safe
2026-06-16 19:10:25 -07:00
Vikhyath Mondreti 8fe090a3a1 fix(input-format): field not editable race condition (#5102)
* fix(input-format): field not editable race condition

* remove dead code

* simplify
2026-06-16 19:03:10 -07:00
Waleed 2ffc004a0a improvement(models): add DeepSeek V4 + Mistral Medium 3.5, fix Codestral context window (#5103) 2026-06-16 18:01:04 -07:00
Theodore LiandClaude Opus 4.8 15a970d805 feat(integrations): hosted email-enrichment providers + cascade wiring (#5087)
* feat(integrations): hosted email-enrichment providers + cascade wiring

Add Datagma, Dropcontact, LeadMagic, Icypeas, and Enrow integrations —
tools, blocks, brand icons, and BYOK + metered hosted-key support — and
register each in the tool/block registries and BYOK provider list.

Wire the new finders/verifiers into the enrichment cascades:
- work-email: Datagma, LeadMagic, Dropcontact, Icypeas, Enrow
- phone-number: LeadMagic, Datagma, Dropcontact
- email-verification: Icypeas, Enrow
- company-info: Datagma, LeadMagic
- company-domain: Datagma

Add hosting tests for all five providers and cascade tests covering the
new providers (incl. new test files for email-verification, company-info,
and company-domain).

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

* fix(enrichment): address PR review on Icypeas success + Datagma billing

- Icypeas find_email/verify_email postProcess return success:true for all
  terminal statuses (NOT_FOUND/DEBITED_NOT_FOUND included) so the cascade
  runner calls mapOutput and records invalid/not-found verdicts instead of
  throwing and inflating the error count
- Bill Icypeas verify FOUND (not just DEBITED*) per the documented 0.1-credit
  charge
- Datagma enrich_person only applies the 30-credit phone surcharge when a
  phone lookup (phoneFull) was requested
- Note Datagma's URL-param (apiId) auth in the hosted-key doc comment
- Update hosting tests to match

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

* fix(enrichment): only bill Enrow verify on a completed verification

getCost returned a flat 0.25 credits regardless of output, so a job that
fell back to the initial submit response (poll never completed, no
qualification) was still metered. Charge 0.25 only when a qualification is
present; 0 otherwise. Add a no-qualification test case.

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

* chore(enrichment): peg hosted credit cost to each provider's lowest paid plan

Align *_CREDIT_USD to the entry tier Sim will provision:
- Datagma: Regular $49/3,000 emails → $0.0163 (was Popular $0.0132)
- LeadMagic: Basic $49/2,000 → $0.0245 (was Growth $0.0104)

Icypeas (Basic $0.019), Enrow (Starter $0.012), and Dropcontact (Starter
~$0.17) already reflect their lowest plan. Tests derive from the constants,
so values stay consistent.

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

* fix(enrichment): address PR review on mononyms + Icypeas verify email map

- work-email LeadMagic: pass full_name + domain so single-token (mononym)
  names are no longer skipped
- work-email Icypeas: firstname/lastname are optional on the API, so run a
  mononym with firstname alone instead of self-skipping
- icypeas_verify_email mapItem reads item.email (verify payload shape) with a
  fallback to the nested results.emails[0].email

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

* fix(enrichment): case-insensitive Enrow find billing

getCost compared qualification to exactly 'valid' while the cascade
normalizes with toLowerCase(), so a differently-cased API qualifier could
zero out billing on a valid email. Lowercase before comparing; add a test.

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

* fix(enrichment): drop Dropcontact from the phone-number cascade

Dropcontact is an email/company-data enrichment service, not a phone-discovery
provider — its phone/mobile_phone fields are unreliable and were surfacing
firmographic data (an employee-count range like "5000-20077") as the phone.
Keep the two purpose-built phone finders (LeadMagic find_mobile, Datagma
find_phone); Dropcontact stays in the work-email and company cascades where
its data is reliable.

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

* fix(enrichment): only accept valid-qualified Enrow emails in work-email

Enrow's finder qualifies each email valid/invalid. The work-email mapOutput
accepted any non-empty email, so an invalid-qualified address could fill the
cell while hosted billing (which only charges on valid) charged zero. Gate the
cell on qualification === 'valid', consistent with billing.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 19:12:59 -04:00
Vikhyath Mondreti feca5fa6f6 improvement(execution, connectors): offload large function inputs, increase connector limits + better error propagation (#5089)
* fix(execution,connectors): offload large function inputs; harden KB connector size limits

Addresses a class of 10 MB limit failures:

- executor/variables: offload over-budget function block-output context values to
  durable large-value refs (lazy `sim.values.read`) so JS function blocks can merge
  medium files without exceeding the 10 MB inter-block request-body cap.
- connectors: stream downloads via `readBodyWithLimit` (memory-safe), and surface
  oversized files as visible `failed` KB documents instead of silently dropping them
  — listing-time for github/s3/dropbox/onedrive/sharepoint, fetch-time for
  gitlab/azure/google-drive via a shared `ConnectorFileTooLargeError`. Raise the
  per-file cap from a hardcoded 10 MB to the canonical 100 MB KB document limit
  (`CONNECTOR_MAX_FILE_BYTES`), except Google Drive's export path (Google's hard
  10 MB export-API limit).
- sync-engine: `classifyExternalDoc` + bulk `skipDocuments` (failed rows with a
  reason, excluded from retry), byte-bounded batch concurrency to cap peak worker
  memory at the raised cap, and a `metadata.fileSize ?? size` fallback.

* fix zoom

* update skill

* address comments + fix terminal event in sse stream

* fix accounting issue
2026-06-16 14:22:59 -07:00
Waleed cc56408be3 perf(execution): parallelize preflight gates, cache deployed state, memoize Anthropic client (#5098)
* perf(execution): parallelize preflight gates, cache deployed state, memoize Anthropic client

- Memoize Anthropic + Azure-Anthropic SDK clients (new client-cache.ts) keyed
  by apiKey (+beta header; +baseURL/version/pinnedIP for Azure) so HTTP
  keep-alive connections are reused instead of a fresh TLS handshake per call.
  apiKey is the tenant boundary.
- Parallelize the read-only preflight gates in preprocessing.ts (ban +
  subscription, then usage + org-member + rate-limit) while preserving exact
  error precedence (ban 403 -> usage 402 -> rate 429) and keeping the sole
  write (admission reservation) last.
- Parallelize the independent workflow-state and env-var loads in execution-core.
- Cache deployed workflow state by immutable deploymentVersionId with
  deep-clone-on-read, oldest-first eviction, and a 5-min TTL bounding the
  credential-mapping edge across ECS tasks.
- Parallelize the independent personal-subscription + membership queries in
  getHighestPrioritySubscription.
- BYOK: drop the redundant getWorkspaceById existence check (auth already
  validates the workspace); read the key list fresh every call for zero
  cross-instance staleness.

Billing/usage/ban/permission reads stay fresh on the primary (no cache, no
replica). Adds tests for every new mechanism and fixes a pre-existing vitest
class-mock incompatibility that had execution-core.test.ts fully red on staging.

* fix(execution): run rate-limit gate only after ban/usage pass

The rate-limit gate is not read-only — checkRateLimitWithSubscription consumes
a token — so running it in parallel with the read-only gates debited rate-limit
quota for requests that the ban (403) or usage (402) gates reject, which the
original sequential flow never did.

Move the rate-limit gate to run sequentially after the ban and usage gates pass,
preserving the read-only gates' parallelism (ban + subscription + usage) and the
exact ban -> usage -> rate precedence. Add regression tests asserting the rate
limiter is not consumed when an earlier gate rejects, and is consumed once when
they pass.

Caught by Cursor Bugbot review.

* chore(execution): trim redundant preflight comments

Tighten the gate overview to match the sequential rate-limit gate and drop
inline notes that duplicated it or the runRateLimitGate doc.

* refactor(cache): address review — idle TTL for client cache, LRUCache for deployed state

- client-cache: add updateAgeOnGet so the TTL is genuinely idle-based (active
  clients keep their warm keep-alive connections; the JSDoc now matches behavior).
- deployed-state: replace the hand-rolled Map + manual FIFO eviction/TTL with
  LRUCache (real LRU eviction, built-in TTL), matching the effectiveDecryptedEnv
  and integration-tool-schema caches. TTL stays absolute (not reset on read) so
  the credential-migration remap still propagates across ECS tasks.

Both per review feedback from Greptile.

* test(execution): isolate rate-limit gate test from STEP 7 reservation

The 'consumes the rate-limit gate once' test reached the STEP 7 admission
reservation, which depends on Redis — it passed locally (reserve throws and is
swallowed) but failed in CI (reserve returns not-reserved -> 429). Pass
skipConcurrencyReservation so the test isolates the rate gate deterministically.

* perf(providers): memoize SDK clients where the pool is per-client (bedrock, vllm)

Generalize the Anthropic client cache into one shared memoizer
(providers/client-cache.ts) and apply it only where each new client owns its own
connection pool — so reuse actually keeps connections warm:

- bedrock: AWS SDK clients hold a per-client connection pool (reuse is the AWS
  best practice). Keyed by region + credential identity.
- vllm: a pinned endpoint creates its own undici Agent per call; key by the
  resolved IP so DNS re-validation still runs each request.
- anthropic + azure-anthropic: migrated onto the shared memoizer.

Deliberately NOT applied to the OpenAI-compatible providers, groq, cerebras, or
google: their SDKs share a process-global keep-alive pool (Node openai-sdk module
singleton agent; anthropic/global undici), so a fresh client per request already
reuses connections and memoization would add complexity with ~no benefit. litellm
uses a plain shared-agent client (no pinning) and is likewise skipped.

Bounded LRU (max 1000, 30m idle TTL) with no close-on-eviction, avoiding the
unbounded-growth and eviction-closes-in-use-client failure modes seen in similar
client caches.

* chore(perf): trim verbose comments to terse why-notes

* chore(perf): drop obvious inline comments, keep nuance as TSDoc

* fix(bedrock): key client cache on full credential, not just access key id

A corrected secret under the same access key id would otherwise keep serving the
stale cached client until TTL/eviction. Caught by Cursor Bugbot.

* test(execution,providers): fix preflight mock reset + isolate provider client cache in tests

- preprocessing.test: re-establish the checkOrgMemberUsageLimit mock in beforeEach
  (the only gate mock not re-set). In the full suite its implementation was reset
  so the success-path test got undefined -> threw -> 500 -> success:false. Mirrors
  how checkServerSideUsageLimits is handled.
- client-cache: add clearProviderClientCacheForTests; call it in the bedrock and
  vllm test beforeEach so construction assertions always start from a cache miss
  now that those providers memoize their client.

* test(execution): make RateLimiter mock constructable under vitest 4.x

The RateLimiter mock used an arrow factory (vi.fn(() => ({...}))). vitest 4.x
(CI) rejects `new` on an arrow-implemented mock ("not a constructor"); 3.2.4
allowed it. The new rate-gate test is the first to actually `new RateLimiter()`,
so it surfaced the failure only in CI. Switch the mock to a regular function and
drop the speculative beforeEach re-establishments that didn't address it.
2026-06-16 13:49:03 -07:00
Waleed f238184fa0 feat(file): add Compress and Decompress operations to the File block (#5100)
* feat(file): add Compress operation to bundle files into a .zip archive

* feat(file): add Decompress operation to extract .zip archives

Adds the inbound half of the archive pair: extracts a .zip back into the
workspace with zip-slip path sanitization, symlink skipping, and entry/
size caps to bound zip-bomb expansion. Extracted files are returned in the
files output, ready to chain downstream.

* fix(file): align archive ops with v5 output surface and zip mime

- Drop the single 'file' output reintroduced for compress/decompress; v5
  intentionally exposes only 'files' (plus id/name/size/url scalars), so
  compress/decompress reuse the existing surface with no new block output
- Add zip/gz to EXTENSION_TO_MIME (previously only in the reverse map), so
  archive extensions resolve to a real mime instead of octet-stream
- Update File v5 block test for the two new operations

* fix(file): harden compress naming per review

- Flatten zip entry names to a safe basename so untrusted fileInput names
  with .. or / cannot produce zip-slip entry paths (cursor)
- Treat archiveName as a flat name landing at the workspace root instead of
  passing it through splitWorkspaceFilePath, which silently created folders
  for names with separators (greptile)
- Add the upfront empty-input guard before any DB calls, matching the read
  and content operations (greptile)

* fix(file): make decompress extraction atomic and bound per-entry size

- Read and validate every entry before writing any file, so hitting a size
  cap no longer leaves partially-extracted files in the workspace (cursor)
- Enforce the per-entry cap on the materialized buffer in addition to the
  declared size, covering entries that omit an uncompressed size (cursor)
- Pre-check declared sizes up front to reject standard zip bombs before
  materializing, and return 422 when no files could be extracted (cursor)

* fix(file): exclude skipped entries from caps and reject multi-archive decompress

- Resolve safe (sanitized) zip entries up front so unsafe/skipped entries
  no longer count toward the per-entry and total uncompressed-size caps (cursor)
- Reject decompress input that resolves to more than one archive with a clear
  error instead of silently extracting only the first (cursor)

* fix(file): enforce single-archive decompress at the API boundary

The block already rejects multiple archives, but the manage route is the
real boundary (callable directly and by the LLM tool) and still took the
first of multiple resolved inputs. Add the empty-input and >1-archive guards
in the route so extra archives are rejected with a clear error rather than
silently ignored (cursor).

* docs(file): correct compress description and stale file-output references

- Drop the misleading 'under provider upload limits' claim from the compress
  tool description (models cannot read zip archives)
- Fix bestPractices to reference the 'files' output, not a non-existent 'file'
- Remove the stale 'file' property from the compress test fixture so it
  matches the real API response (greptile)
2026-06-16 12:25:33 -07:00
Waleed c864a928bf improvement(models): sort model dropdown by latest release date within each provider (#5099)
* improvement(models): sort model dropdown by latest release date within each provider

* fix(models): preserve input provider order and build catalog index once
2026-06-16 11:21:21 -07:00