The SSRF-guarded fetch used for MCP OAuth (discovery, DCR, token exchange/
refresh, RFC 7009 revocation) created a fresh pinned undici Agent per request
and never tore it down, and returned the live response so the SDK's lazy body
read happened outside any deadline. The MCP SDK sets no timeout on OAuth legs,
so a stalled body read or a leaked keep-alive socket could leave the flow — and
the browser waiting on it — pending indefinitely ("Connecting… forever").
- Buffer the (always-small) OAuth JSON body inside the guard, under the composed
deadline/caller AbortSignal, then return a detached in-memory Response. undici's
bodyTimeout only measures idle gaps between chunks and cannot bound a slow-drip
body; the AbortSignal is the only true wall-clock deadline over the body read.
- Destroy (not close) the per-request pinned Agent on every path so a one-shot
leg can't strand its keep-alive socket, and teardown itself can't hang.
- Fix the same per-request Agent leak in the auth-type probe.
- Returning a normalized global Response also surfaces provider token-endpoint
errors cleanly instead of the SDK's opaque parse failure.
* zip checkpoint
* fix(zip-uploads): harden extract path from review findings
- Replace the whole-buffer central-directory signature scan with an
EOCD-anchored walk (shared with zip-guard) so zips containing STORED
nested archives are no longer falsely rejected, and report the accurate
cap (new 'central_dir_too_large' reason) instead of 'Maximum is 1000'
- Make extraction all-or-nothing: a discarding validation pass inflates
every entry against the caps before anything is uploaded, so lying
headers or corrupt entries can't leave partial trees (corrupt DEFLATE
streams now surface as ArchiveError instead of raw zlib errors)
- Single-source ArchiveError messages; callers surface err.message and
map only status/reason (removes the three divergent message maps)
- Guard save/import against archives (saving stranded the contents),
extend the workspace ownership check to all three operations, dedupe
fileNames, and refuse re-extracting into a non-empty folder instead of
duplicating the tree with ' (1)' copies
- Harden the extraction folder name (dot segments, control chars,
separators) and compute the destination before extracting
- Sniff small '.zip'-named uploads so mislabeled text files stay
readable instead of dead-ending between read and extract
- Scope zip acceptance to the mothership flow only (attachment list,
accept attribute, upload/presigned gates) so execution/workspace/chat
surfaces keep rejecting zips up front
- Don't count skipped noise entries toward the 1000-file cap; restore
per-entry zip-slip forensics via skippedUnsafePaths logging
- Teach materialize_file(fileNames: [...]) in the extract guidance to
match the declared schema
* fix(zip-uploads): address review follow-ups on the extract path
- Roll back already-uploaded files when an upload fails mid-extraction
(storage/DB error, quota crossed), so callers and retries never observe
a partial tree
- Fold reserved system folder names (.changelogs, .plans) into the
'archive' fallback so extraction can't write into alias-backing
namespaces or bypass the already-extracted lookup that hides them
- Align the archive byte-sniff budget with the read path's inline text
cap (5MB), so any mislabeled '.zip' small enough to be read inline is
sniffed and read instead of dead-ending
* fix(zip-uploads): detect prior nested-only extractions in the re-extract guard
The already-extracted check only looked for files directly inside the
archive root folder, so a zip whose entries are all nested (src/index.ts)
left only subfolders there and a second extract slipped past the guard,
duplicating the tree with ' (1)' suffixes. Extraction roots its whole tree
at that folder, so a prior run always leaves a direct file OR a direct
subfolder — check both.
* feat(blocks): surface deprecated block and model warnings on canvas
* improvement(blocks): simplify deprecation derivation, drop unused getModelReplacement
* fix(blocks): make deprecation badge keyboard-accessible
* diag(mcp): comprehensive HTTP logging for OAuth + streamable-HTTP transport
Temporary diagnostic to root-cause the Gauge MCP 'initialize' hang. Wraps both the
OAuth guarded fetch and the transport pinned fetch to log every request/response
with timing: method, url, status, content-type, mcp-session-id, safe headers, and —
for the transport phase only — the response body streamed chunk-by-chunk. So one
authorize reveals exactly what Gauge returns for initialize (SSE that stalls vs
never-sent result vs fast JSON) and where it stalls.
Enabled by default; MCP_HTTP_DIAGNOSTICS=false to silence; disabled under test.
Never logs request bodies or the OAuth token-response body (carry tokens); every
logged value passes through sanitizeForLogging. Revert once root-caused.
* diag(mcp): scope body logging to initialize + redact URLs + wrap unpinned
Review fixes (Greptile/Cursor):
- Only stream-log the response body whose REQUEST is an MCP initialize JSON-RPC
call. The transport fetch also carries in-transport OAuth refresh and every
tools/call result, so gating on phase alone leaked token responses and tool
output (PII/file contents). initialize gating excludes both.
- Redact URL query strings (?code=/?token=/?api_key=) — log origin+path only.
- Wrap the transport fetch whether pinned or not (SDK default is globalThis.fetch,
so passing it is behavior-neutral) so unpinned/allowlisted servers are covered too.
* diag(mcp): sanitize initialize preview + log at warn for visibility
Review fixes (Cursor):
- Run the initialize body preview through sanitizeForLogging (defense-in-depth
against a server stuffing token-like values into serverInfo/capabilities).
- Log diagnostics at warn, not info, so they surface even if an environment's
LOG_LEVEL drops info (staging runs INFO, but this removes the dependency).
* diag(mcp): reuse sanitizeUrlForLog + cancel log reader on abort
Review fixes (Cursor):
- Replace the local safeUrl helper with the shared sanitizeUrlForLog so URL
redaction/truncation stays consistent.
- The detached initialize-body log reader now observes init.signal and cancels
its tee-branch reader on abort, so it can't keep the response stream / connection
alive after the SDK's initialize timeout gives up (the hang we're tracing).
* diag(mcp): length-gate initialize check to skip parsing large tool payloads
isInitializeRequest now rejects bodies over 4096 chars before any JSON.parse. The
initialize message is a small fixed structure, so this keeps large tools/call
payloads (potentially multi-MB) off the parse hot path while still detecting
initialize.
* improvement(ux): submit on Enter in chip modals and advance table rows
* fix(emcn): stop Enter double-submit and close registration gap
- stopPropagation on field Enter-submit so a parent onKeyDown can't re-fire the primary (double OAuth connect, etc.)
- register primary via useLayoutEffect so it's set before paint (no null-Enter window)
- drop now-redundant parent Enter handlers in connect-oauth/create-workspace/rename-document modals
* fix(table): suppress auto-opened tag dropdown when Enter advances cells
Focusing an empty cell auto-opens the tag dropdown; without closing it a follow-up Enter inserts a tag instead of navigating down the column.
* fix(table): clean up cell refs on unmount and guard Enter advance
- delete inputRefs/overlayRefs entries when a cell detaches so deleting a row can't leave stale position-keyed entries
- guard Enter advance with isConnected so a stale ref can never steal focus into a detached node
* feat(pi): add Cloud Code Review mode and rename Cloud to Cloud PR
Introduce a third Pi mode that reviews an existing GitHub PR in an E2B sandbox and posts a structured review with optional inline comments. Keep the stored cloud id for backward compatibility, extend github_create_pr_review for inline comments, and harden review submission against stale SHAs and invalid comment payloads.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(pi): cleanup code
* address comments
* address mor
* address comments
* update
---------
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
* fix(mcp): bound OAuth discovery/DCR/token fetches with a timeout
The MCP SDK issues OAuth discovery, dynamic client registration, and token
exchange with a bare fetch and no AbortSignal (only the JSON-RPC layer gets the
SDK's request timeout), and undici's default headers/body timeouts are 5 min. A
slow or unresponsive authorization server therefore left /oauth/start pending for
minutes — the browser stuck on "Connecting…" forever.
Bound each guarded OAuth/revocation leg with a 30s AbortSignal.timeout, composed
with any caller signal so cancellation still works. Only our own deadline is
relabeled to an McpError; caller aborts and other failures propagate unchanged.
Scoped to createSsrfGuardedMcpFetch (OAuth/revoke/probe) — the live transport's
timeouts are untouched.
* fix(mcp): bound SSRF/DNS validation by the deadline too
Move the timeout signal ahead of validateMcpServerSsrf and race the validation
(whose dns.lookup takes no signal) against it, so the deadline covers the whole
guarded call — a stalled DNS resolution now rejects at timeoutMs instead of the
OS DNS timeout. Listener is cleaned up on settle so a late timeout can't surface
as an unhandled rejection.
* fix(mcp): compose caller signal before validation + attribute timeout by reason
Compose the caller's AbortSignal with the deadline up front and use the combined
signal for both SSRF validation and the HTTP request, so caller cancellation now
covers the whole guarded call (previously a caller abort during a stalled DNS
validation waited for the full deadline). Attribute the timeout relabel by the
rejection reason's identity rather than init.signal's state, so a caller signal
that aborts just after the deadline can't misattribute a genuine timeout.
* fix(mcp): adopt in-flight validation on early abort to avoid unhandled rejection
When raceWithSignal is entered with an already-aborted signal it returned without
attaching to the in-flight validateMcpServerSsrf promise, so a later SSRF/DNS
rejection could surface as an unhandled rejection. Swallow the orphaned promise's
settlement in the early-abort branch.
* test(mcp): use sleep() instead of raw setTimeout in pinned-fetch test
check:utils bans new Promise(resolve => setTimeout(...)) in favor of sleep() from
@sim/utils/helpers.
* improvement(blocks): name the integration in every suggested-action template title
Home-screen "Suggested actions" rows render only an icon + the block
template title, so titles that did not name their integration (e.g.
"Refund pattern monitor") were unidentifiable. Rewrite the 234 titles
that omitted their integration so each names it naturally, matching
each block's existing "Brand + phrase" style. Titles-only change; the
catalog surfaces that already show the integration as page context are
unaffected.
* fix(blocks): name PagerDuty-triggered incident template after its icon, not owner
The Confluence-owned incident template uses a PagerDutyIcon, is triggered
by PagerDuty, and is cross-listed to notion/pagerduty/datadog/slack, so
prefixing the owner name misidentified it on those surfaces and fought
its own icon. Name it after the icon + trigger integration instead.
* fix(blocks): align suggested-action icons with titles and de-dupe Firecrawl names
For rows where the owner-prefixed title named a different integration than
the row's icon (github/Notion, stripe/Sheets, calendar/Twilio, gmail/Lemlist,
slack/Linear, linear/Slack), swap the icon to the owner integration the title
already names so the icon-only home row is coherent; drop the now-unused icon
imports. Differentiate two Firecrawl titles that collided with existing
near-duplicate templates.
* fix(blocks): de-duplicate suggested-action titles the brand prefix collided
Renamed a handful of titles whose brand prefix made them near-identical to a
sibling template: google_translate (Intercom replier / ticket auto-reply),
sentry (on-call triage agent), google_drive (personal notes assistant), sqs
(alert enricher), typeform (survey summarizer). Swept every renamed block for
the same near-duplicate class.
* fix(blocks): align Greptile Slack Q&A bot icon with its title
The renamed title leads with Greptile but the row kept icon: SlackIcon,
conflicting on the icon-only home surface. Swap to GreptileIcon (matching
the block's sibling templates) and drop the orphaned SlackIcon import.
The MCP OAuth callback fails with invalid_state because the authorization
state is cleared/missing by the time the user authorizes — but clearState was
silent, so the clobber never surfaced in logs. Log every state save and clear
with a caller context so the exact source is visible on the next repro.
Implement the sim-side share_file server tool (resolves VFS path to file,
keeps the existing org-policy + permission + audit checks, delegates to
upsertFileShare). Register it in the tool router and generated catalog.
Stamp an ambient 'shared'/'shareAuthType' flag onto file metadata via one
batched share lookup, mirroring how workflows expose 'isDeployed'.
Validate the EFFECTIVE auth type when re-enabling a share: upsertFileShare
preserves an existing share's authType when none is passed, so validate the
stored mode (not 'public') or a re-share could reactivate a now-disallowed
password/email/sso share. Same fix applied to the share PUT route.
* Simplify TikTok to draft-only Content Posting
Remove Direct Post stubs and legacy Share Kit webhook triggers that Sim does not ship, and fix draft upload response handling so real TikTok errors are not misreported as size-limit failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix TikTok cleanup lint and Greptile review findings
Reject legacy mode:direct publish requests instead of silently drafting, keep deprecated Share Kit triggers registered for saved workflows, and apply biome format/import fixes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Finish TikTok greenfield draft-only surface
Remove Query Creator Info and video.publish, drop Share Kit trigger stubs, rename publish-video to upload-video-draft, and strip leftover Direct Post mode from the contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Hide TikTok from the toolbar until app review is approved.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(mcp): deliver OAuth callback result over BroadcastChannel so COOP can't strand the connect
An MCP provider whose authorization page sets `Cross-Origin-Opener-Policy:
same-origin` (e.g. Gauge) severs `window.opener` when the popup navigates
through it, so the callback's `window.opener.postMessage` was silently lost and
the row hung on "Connecting…" forever — even when the callback succeeded.
- Signal completion over a same-origin `BroadcastChannel` instead, which is
origin-scoped and immune to opener severance (the MDN/Chrome-recommended COOP
workaround). Scope the message by workspaceId so other open workspaces ignore it.
- Log every OAuth callback failure with its reason + serverId. The early-return
gates previously returned a silent `ok:false` popup close, so a failed
authorization was undiagnosable from the server logs.
* fix(mcp): react to the OAuth broadcast only in the tab that opened the popup
A BroadcastChannel reaches every same-origin tab, so the previous workspace-id
scoping (which the success path carried but failure paths omitted) still let
unrelated tabs clear state, refetch, and show spurious toasts. Gate every
reaction on whether this tab actually has an in-flight popup for the result's
server (`popupIntervalsRef`) — strictly more precise than workspace scoping and
correct for both cross-workspace and same-workspace-second-tab cases. Removes
the now-redundant workspaceId from the callback message.
* fix(mcp): decouple OAuth result correlation from popup.closed
Cursor flagged two defects in the previous gate, both rooted in keying the
BroadcastChannel filter on `popupIntervalsRef` (the popup.closed poll map):
- A genuine completion was dropped whenever the poll had already removed the
server's entry — and COOP can make `popup.closed` misreport, which is exactly
the case this PR targets, so the fix could silently fail to apply.
- A result without a serverId fell back to "any in-flight popup", waking
unrelated same-origin tabs with spurious toasts/refetches.
Introduce `pendingFlowsRef` (serverId -> safety timeout) as the correlation
source of truth: cleared only on completion or a 10-min timeout (matching the
server OAuth start TTL), never by popup.closed. The popup.closed poll now only
clears the spinner (best-effort abandon UX) and never touches correlation, so a
real completion is always processed. Results without a serverId are ignored;
the initiating tab's safety timeout clears its own "Connecting…".
* fix(mcp): correlate the OAuth result on the state nonce, not serverId
Cursor flagged that a failure which can't resolve a serverId (notably
invalid_state) broadcasts ok:false with no serverId, so the serverId-keyed gate
ignored it and the initiating tab sat on "Connecting…" until the safety timeout
with no error feedback.
Correlate on the OAuth `state` instead — the per-flow nonce the callback echoes
on every result, success or failure. The client parses it from the authorization
URL and keys the in-flight map by it; the callback includes it on every response
via a `respond` helper. This is the canonical popup-OAuth correlation: it reaches
the initiating tab even when no serverId exists, and — because each flow has a
unique state — it also fixes the same-user-same-server-in-two-tabs edge a serverId
key left open. Results with no parseable state (a malformed callback) are still
ignored; that flow clears via its timeout.
* fix(mcp): drop a server's prior in-flight OAuth flow when it is retried
Each start mints a new `state`, so an abandoned attempt's safety timeout was
keyed under a different state than its retry and never cleared. In the contrived
case where both stayed pending ~10 min, the stale timer would clear the newer
flow's spinner. Sweep any prior in-flight flow for the same serverId on a new
start (replacing the can't-happen same-state check). Result delivery was already
correct; this makes the state machine airtight.
Response-side Zod .catch() tolerance on the three strict-enum-over-free-text MCP columns (transport/authType/connectionStatus) so one legacy row can't fail the whole server-list validation; fork copy normalizes transport; create/upsert path resets connection status on any auth-type flip (mirrors update path); bulk discovery drops the positive tool cache on OAuth-pending. Request bodies stay strict.
* feat(mcp): reuse warm connections for tool execution and discovery
* fix(mcp): make connection pool concurrency-safe and auth-scoped
* fix(mcp): decouple pool validity from updatedAt, widen dead-connection detection, guard ping race
* fix(mcp): retire pooled connections on auth failure and server delete
* improvement(mcp): defer bulk-discovery env resolution to the pool miss path
* fix(mcp): retire in-flight creates evicted mid-connect via server generation
* fix(mcp): use evicted-mid-create connections one-shot and decouple pool eviction from cache clear
* improvement(mcp): dedupe create thunks into buildClient, harden dispose against liveness race
* fix(mcp): close acquire-gap races (re-resolve after stale ping, skip closing entries)
* fix(mcp): retry auth failures in executeTool; simplify acquire re-check and clear generations on dispose
* fix(mcp): let bulk discovery reconnect a lost notification connection with current config
* fix(mcp): recover rotated credentials on discovery via shared fetchServerTools auth-retry
* chore(mcp): add debug logging for pool hit/miss/eviction observability
* improvement(mcp): negotiate HTTP/2 for MCP transport
MCP servers are commonly behind HTTP/2 fronts (CDNs, cloud LBs), but the
SSRF-pinned undici Agent is HTTP/1.1-only unless it opts into h2 via ALPN. Add
an allowH2 option to createPinnedFetch (default false, so LLM-provider callers
are unchanged) and centralize the MCP h2 decision in one createPinnedMcpFetch
routed through the transport, OAuth probe, and SSRF-guarded fetch. Pinning is
unaffected: the pinned lookup forces every connection to the resolved IP.
* fix(mcp): correct auth-type handling, OAuth-pending UX, and safe error logging
- Classify a non-OAuth UnauthorizedError as an auth failure instead of an OAuth
redirect, so static-bearer servers that merely advertise OAuth stop being
diverted into the OAuth flow (fixes the class of server that couldn't connect).
- Reset a server to disconnected when it is switched to OAuth, so it can't
falsely read as connected before completing its auth flow.
- Surface OAuth-pending state as 'OAuth authorization required' in the server
list and refresh action instead of a generic 'Not Connected'.
- Return an actionable 422 for OAuth servers that don't support dynamic client
registration.
- Redact error messages/cause/session-ids from MCP transport/connect logs via a
shared getMcpSafeErrorDiagnostics helper.
* fix(mcp): reset connection on any auth-type flip, scope h2 to live transport
* fix(mcp): close pinned h2 Agent on disconnect and revoke OAuth tokens on auth-type change
* fix(mcp): release pinned Agent on failed connect and point DCR error at client-id/secret setup
* fix(mcp): make pinned Agent teardown idempotent to avoid double-destroy on cancel/failed connect
* fix(security): bound YAML alias expansion in file-parser to prevent DoS
The YAML parser called yaml.load() then JSON.stringify() on the result.
YAML aliases (*anchor) resolve to shared references, so yaml.load cheaply
builds a compact DAG, but JSON.stringify expands that DAG into a full tree,
duplicating every shared node. A crafted sub-1KB .yaml/.yml document could
therefore expand to hundreds of MB / GB during serialization, pinning CPU
and OOM-killing the shared parse/ingestion worker (CWE-776).
Add a bounded, iterative traversal that runs before JSON.stringify and
aborts once expanded node count, estimated serialized size, or nesting
depth exceeds safe caps. This detects the amplification against the compact
DAG before any allocation, and also terminates cyclic anchor structures.
Replaces the unbounded recursive getYamlDepth (which spread large arrays
into Math.max(...array), risking a stack overflow) with the same bounded
walk.
* harden YAML guard: charge keys + escaping, bound stack, fail closed
Addresses review findings on the alias-expansion guard:
- Charge object keys, not just values. JSON.stringify re-emits every key on
each alias expansion, so an aliased object with a long key amplified without
being counted. Keys are now charged against the size cap.
- Compute the exact JSON-escaped string length (quotes, backslashes, control
chars) instead of a flat multiplier. Plain text is charged its true 1:1 size
(no false rejection of large legitimate documents) while escape-heavy strings
are charged their real, larger cost (no cap bypass).
- Count each node as it is enqueued and only push container nodes onto the
traversal stack. A pathologically wide fan-out now trips a cap during the
enqueue loop instead of first materializing millions of stack entries and
exhausting memory inside the guard itself.
- Fail closed in POST /api/files/parse: a YamlComplexityError rejection is now
re-thrown (mirroring isPayloadSizeLimitError) rather than silently falling
back to storing the crafted document as raw text.
* yaml guard: charge lone surrogates at their escaped length
serializedStringLength treated surrogate code units as cost 1, but
well-formed JSON.stringify escapes a lone surrogate to \uXXXX (six units).
Charge lone surrogates as 6 and valid high+low pairs as-is (two units),
matching JSON.stringify exactly. Verified against JSON.stringify across
plain text, escapes, control chars, lone high/low surrogates, and valid
pairs.
* improvement(mcp): align tool discovery timeouts and retries with MCP protocol standard
- Raise tools/list idle timeout 10s -> 30s (derived from per-server config,
clamped to max execution timeout) with a 60s hard cap via maxTotalTimeout,
matching the SDK's DEFAULT_REQUEST_TIMEOUT_MSEC. Enable resetTimeoutOnProgress
with an onprogress handler so slow-but-alive servers are not spuriously failed.
- Retry read-only tools/list on transient transport errors (timeout,
connection-closed, 429/5xx, session 404/400, network) with jittered backoff.
tools/call stays conservative (session-error retry only) since it is not
idempotent. The MCP SDK does not retry POST requests, so the app owns this.
- Wire client.onerror so out-of-band transport failures are observed, not
silently dropped.
- Map timeouts to a user-facing message and surface refresh/remove failures via
the standard emcn toast instead of swallowing them.
* chore(mcp): trim comments to match codebase density
* fix(mcp): don't fail refresh with 500 when post-discovery workflow sync throws
Discovery already persists status and caches tools; a syncToolSchemasToWorkflows
failure was escaping the refactored try/catch and returning 500 despite the
refresh having succeeded. Guard the secondary sync so it degrades to zero
workflows updated instead.
* fix(mcp): keep tools/list absolute timeout ceiling hard
A configured per-server timeout above 60s was expanding maxTotalTimeout past
the intended absolute discovery ceiling. Clamp the idle timeout to the ceiling
too so tools/list can never hang the UI beyond it; connect() and callTool()
still honor the full configured/execution timeout.
* perf(mothership): restore static markdown parse for settled chat messages
Settled/reloaded chat messages rendered through Streamdown's streaming
parser (remend + incomplete-markdown repair + per-block re-parse, running
the rehype raw/sanitize/harden chain once per block). Because rows are
virtualized, every up/down scroll remounted the messages crossing the
overscan boundary and re-paid that N-block cost — the scroll lag.
- Render never-streamed mounts (reloaded history, or an in-session message
scrolled out of the virtualized window and back) with mode='static': one
whole-document parse instead of streaming's per-block re-parse. In-session
streaming keeps the streaming parser for its mount life (no drain flash).
- Cache Prism highlight output in a module-level bounded LRU so a code block
re-highlights at most once across the unmount/remount virtualization does
on scroll (a component useMemo would not survive the unmount).
* fix(mothership): don't cache fallback-highlighted code blocks
An unregistered language highlighted via the JavaScript fallback was cached
under its own name, so if that Prism grammar registered later in the session
a remount would keep serving the stale fallback render. Resolve the grammar
inside the highlight helper and skip the cache entirely on the fallback path.
* fix(confluence): index mirrored/included page content via rendered view format
The KB connector fetched page bodies as body-format=storage, which only
carries unexpanded macro references (Include Page / Excerpt Include). Those
'mirrored' articles were stripped to empty content by htmlToPlainText and never
synced. Switch getDocument to body-format=view (supported on the v2 single-item
page/blogpost GET) so built-in include/excerpt macros render inline and the
included text is indexed.
* fix(confluence): invalidate existing doc hashes on representation change
The version-based contentHash meant already-synced mirrored documents (with
stale empty content) classified as 'unchanged' and never re-hydrated with the
new rendered view content. Embed a body-representation marker in the hash so a
representation change invalidates every previously-synced Confluence document,
forcing a one-time re-hydration that picks up the expanded include/excerpt text.
* feat(connectors): full resync re-hydrates rendered content (transclusions)
Version-based change detection can't see when a Confluence page's rendered view
changes because an *included* page was edited (the container's version doesn't
bump). Add a 'Full resync' path so that drift can be recovered:
- ConnectorMeta.rehydrateOnFullSync flag (set for Confluence)
- on fullSync, classifyExternalDoc promotes unchanged deferred docs to update and
the hydration guard re-indexes unconditionally, so rendered content is refreshed
- fullSync threaded through the manual sync contract (query param), route, and hook
- 'Sync now' / 'Full resync' dropdown on the connector card
Incremental syncs stay hash-gated and cheap; only the deliberate full resync pays
the re-index cost.
* refactor(connectors): decouple rehydrate from fullSync deletion semantics
The transclusion refresh only needs re-hydration, but reusing the fullSync flag
also activated its deletion-cleanup semantics — which bypass three previously
unreachable safety guards (empty-listing wipe, listingCapped, and the >50%
mass-deletion threshold). Since fullSync had no caller before this PR, the new
'Full resync' button would have exposed all three to any KB editor.
Introduce a dedicated 'rehydrate' request that ONLY forces re-hydration + re-index
of already-synced docs. Listing and deletion reconciliation are identical to a
normal sync (all safety guards stay armed). fullSync's cleanup semantics remain
dormant and untouched.
* refactor(connectors): tidy rehydrate flag + gate Full resync to supported connectors
Cleanup/simplify pass over the connector changes:
- use shared booleanQueryFlagSchema for the rehydrate query param (typed boolean
at the boundary instead of a hand-rolled 'true'/'false' string enum)
- move rehydrateOnFullSync onto the client-safe ConnectorMeta so the UI can gate on it
- only Confluence (rehydrateOnFullSync) shows the Sync now / Full resync dropdown;
every other connector keeps its original one-click sync button (Full resync is a
no-op for them, and this restores the pre-change one-click UX)
- wrap the sync trigger in a span so its tooltip still shows while disabled (cooldown)
* fix(connectors): forward rehydrate flag through the Trigger.dev worker
executeConnectorSyncJob (the production async sync path) destructured only
fullSync from the payload and forwarded only fullSync to executeSync, silently
dropping rehydrate. A manual Full resync would therefore never re-hydrate on the
default Trigger.dev path. Forward rehydrate too.
* fix(connectors): rehydrate forces a full listing so containers aren't omitted
A rehydrate request set forceRehydrate but left listing incremental. For a
connector that is both incremental and rehydrateOnFullSync, an unchanged
container page that transcludes a changed page would be omitted from the
incremental listing and never re-hydrated. Force a full (non-incremental) listing
on rehydrate so every document is seen; deletion-safety guards stay armed (unlike
fullSync). No-op for Confluence, which is already non-incremental.
* feat(billing): show invoice descriptions and cap in-app list at 5
* test(billing): model six-item Stripe page boundary in pagination mocks
* fix(billing): decouple Stripe scan page size from invoice display cap
* feat(gitlab): dynamic access levels + group and membership operations
Access levels can now be bound to runtime references (e.g. a policy-table
lookup), not just picked from a static list. The three access-level fields
(accessLevel, memberAccessLevel, invitationAccessLevel) switch from dropdown
to combobox so a reference expression is accepted at Copilot save-time instead
of being rejected as an invalid enum value. The resolved value is validated at
execution time by coerceGitLabAccessLevel, which accepts an integer, a numeric
string, or a level name ("Developer"), and throws a clear error otherwise -
preserving the real safety property (no bad integer reaches GitLab) while
dropping the false one (levels must be known at design time).
Also closes demand gaps from the AskIT data:
- get_group / list_groups: resolve and list groups (provisioning critical path)
- list_user_memberships: admin GET /users/:id/memberships, gated in its
description; the non-admin path is composing list_members
The access-level enum is now a single source of truth in tools/gitlab/utils.ts
(GITLAB_ACCESS_LEVELS) that the block options and runtime coercion both derive
from, replacing three inline copies.
* fix(gitlab): treat access level 0 ("No access") as a provided value
A runtime reference can resolve to the integer 0, but the block still gated
access-level presence with truthiness: required ops rejected 0 as missing and
optional ops silently omitted it. Add hasGitLabAccessLevel(value) (0 and '0'
are provided; undefined/null/'' are not) and use it for all six presence
gates so "No access" can be granted or set via a reference.
* feat(gitlab): expand group listing filters and harden access-level enum
- list_groups: add visibility, min_access_level, and all_available filters (documented on GET /groups) plus order_by/sort, now surfaced in the block and forwarded in params
- order_by widened to include GitLab's documented 'similarity' value
- access-level enum label 'Minimal Access' -> 'Minimal access' to match GitLab verbatim; coercion already case-insensitive
- min_access_level guard rejects 0 (GitLab floor is 5); reuse the access-level enum for the block dropdown (drops 'No access')
- tests: out-of-enum numeric-string coercion case + full list_groups filter mapping
* fix(gitlab): validate list_groups min-access-level and similarity ordering at execution time
Greptile round 1 (both P1):
- min_access_level: coerce via the shared access-level enum (coerceGitLabMinAccessLevel) and throw on out-of-enum values (31, 999) or 0, so a direct tool call fails loudly instead of sending an invalid filter to GitLab
- order_by=similarity now requires a non-empty search term (GitLab ignores similarity ordering without one); throws a clear error otherwise
- tests for both validations
---------
Co-authored-by: Marcus Chandra <mzxchandra@gmail.com>
* perf(workflow): scope resize CSS-var writes to the container, not :root
Resizing the editor panel, terminal, output panel, and sidebar was still
laggy on large workflows even after the drag handles stopped re-rendering
React per frame. A CDP trace showed the cost was style recalculation, not
JS or layout: ~3.9s of Document::recalcStyle over a 50-move panel drag.
Root cause: each drag frame wrote its CSS variable to document.documentElement.
On a large document (~42k elements) any inline custom-property write on the
<html> root recalculates style for the whole tree — measured at ~77ms per
write, independent of what actually reads the variable (an unused var costs
the same). Writing the same variable to the element that consumes it recalcs
only that subtree (~0.5ms) — a ~150x reduction.
useDragResize now writes the variable to a caller-provided target element
during the drag and reconciles to :root once on release (so on-demand readers
and the pre-hydration script are unchanged): panel -> .panel-container,
terminal -> .terminal-container, output panel -> .terminal-container (both
consumers inherit it), sidebar -> .sidebar-shell-outer. The terminal's
expanded-threshold sync writes store state directly instead of setTerminalHeight
so it no longer touches :root mid-drag.
Measured (near-empty canvas, 50-move drag): panel style+layout 3891ms -> 56ms,
frame p95 91.6ms -> 8.4ms, main-thread blocking 4566ms -> 0ms; terminal
recalc 3899ms -> 16ms, blocking 5558ms -> 0ms; sidebar recalc ~77ms/frame -> ~1ms/frame.
* perf(workflow): keep float boundary-sync live during scoped resize drags
The resize hooks now write their CSS variable to the consuming container
element during a drag (not :root), so use-float-boundary-sync's MutationObserver
on :root no longer fired mid-drag and open floats (chat, search-replace,
variables) only re-clamped on release. Read each boundary dimension from its
scoped element with a :root fallback, and observe the container elements as
well as :root, so an open float tracks the drag live exactly as before.
* fix(workflow): finalize drag on unmount + clamp reads scoped output width
- useDragResize: unmounting mid-drag now runs endDrag (commit + drop the
scoped override) instead of a bare cleanup, so navigating away can neither
lose the resize nor strand an inline override on a surviving target element.
- terminal output-panel clamp: read the live --output-panel-width from the
terminal element first (where a drag writes its scoped override), then :root,
then the store, so a mid-drag terminal/window resize can't clamp against a
stale value.
* fix(workflow): robust drag finalize — last-applied value, sidebar unmount, clamp skip
Addresses three review findings on the scoped-resize change:
- useDragResize: track the last applied value and commit that on release;
only recompute from the pointer event while the target is still connected.
On an unmount the target's rect can be detached, so a layout-reading compute
(e.g. the output panel's) would return a degenerate MIN — committing the last
shown value avoids persisting the wrong width, and keeps flick-safety on a
normal release.
- use-sidebar-resize: finalize on unmount (run endDrag, not bare cleanup) so a
drag interrupted by unmount persists the width and drops the scoped override.
This matters more than for the panel/terminal because .sidebar-shell-outer
lives in the workspace chrome and outlives the sidebar, so a stranded override
would win over :root.
- terminal output-panel clamp: skip while an output-panel drag is active (its
scoped inline override is present). That drag's own compute clamps every frame
against the live terminal rect, and a store-driven :root write here would be
masked by the inline override anyway.
* fix(workflow): sidebar flick-safety + clamp reads committed :root width
- use-sidebar-resize: compute the clamped width synchronously on each pointer
move (storing lastWidth) and defer only the DOM write to rAF, so a fast flick
released before the frame runs still commits the final pointer position
instead of a stale frame or nothing.
- terminal output-panel clamp: when not mid-drag, read the committed
--output-panel-width from :root (written synchronously by the store setter)
rather than the React store value, which lags a render behind the commit;
fall back to the store before any commit. Comment corrected to match.
* feat(email): native Gmail API mail provider for GCP self-hosting
Adds Gmail as a fifth transactional mail provider (Resend → SES → SMTP →
ACS → Gmail). GCP has no first-party SES/ACS equivalent, so the native
Google path is the Gmail API: a service account with domain-wide
delegation impersonates a Workspace sender (GMAIL_SENDER) and posts the
raw RFC 822 message (built via nodemailer's MailComposer — full parity
incl. attachments, replyTo, unsubscribe headers) to the media-upload
messages.send endpoint. The Workspace SMTP relay alternative is
documented against the existing SMTP provider.
* fix(email): review round 1 + audit hardening for Gmail provider
- a 2xx from Gmail with an empty/malformed body no longer surfaces as a send
failure (the mailer's fallback chain would deliver the same email twice);
covered by a regression test
- normalize bare-LF line endings in html/text bodies to CRLF (RFC 822) before
composing the raw message
- add multi-recipient and text-only test cases
* fix(oauth): coalesce slack token refresh per installation and preserve provider refresh errors
* fix(oauth): keep transient refresh failures errorless and size slack lock budgets past the provider timeout
* fix(oauth): let slack refresh followers poll for the lock's full lifetime
* chore(oauth): drop provider refresh-error surfacing to keep this PR slack-only
* fix(oauth): version-guard slack chain writes against concurrent connects
* fix(oauth): clear slack dead flag before connect fan-out
* feat(chat): favicon external links with secure link-preview tooltips
* fix(link-preview): address review findings
- allow http fetches to match advertised http(s) link support
- fix meta content regex to handle apostrophes and either quote delimiter
- hash Redis cache keys so sensitive URLs are not stored verbatim
- add per-user rate limit to the outbound-fetching route
- render siteName-only previews instead of falling back to the URL
* fix(link-preview): redact full URLs from failure logs
* improvement(link-preview): render-time preview fetch, cheerio parsing, cleanup pass
- fetch previews when links render (emcn tooltip shows instantly — hover prefetch had no delay to race); tooltip reads the warmed cache, eliminating the URL-then-preview flash
- parse OG metadata with cheerio (already used server-side) instead of hand-rolled regexes + entity decoding, fixing double-decode and quote-handling classes
- drop the no-longer-needed prefetch hook; remove dead side prop on Tooltip.Content
- extract ExternalLink to a sibling module per component-size guidelines; fix TSDoc placement
* fix(link-preview): https-only previews and full-document parsing
- drop allowHttp: plain-http fetches would reach the URL validator's self-host loopback exception; previews are now explicitly https-only on both server (early null) and client (query never fires for http)
- parse the full capped document instead of truncating at the first <body> substring, which could match inside head scripts/comments and drop metadata
* improvement(chat): render mailto links as plain text
* improvement(workflow): zero-render drag-resize for panel, terminal, and output panel
Port the sidebar's pointer-capture + rAF + CSS-variable drag pattern to
use-panel-resize, use-terminal-resize, and use-output-panel-resize so a
drag writes only --panel-width/--terminal-height/--output-panel-width
per frame and commits to Zustand (one re-render + one localStorage
write) on pointerup. Previously every mousemove dispatched a store set,
re-rendering the whole always-mounted Panel tree (Chat/Editor/Toolbar)
and the terminal, plus a persist localStorage write per move. Also drop
the Panel's unused panelWidth subscription and drive the output panel
width via a CSS variable instead of React state.
* improvement(workflow): shared useDragResize hook + review fixes
Extract the drag mechanism into a shared useDragResize hook (pointer
capture, rAF-aligned apply, commit-on-release) consumed by the panel,
terminal, and output-panel resize hooks. Fixes from adversarial review:
commit the last computed value instead of reading the CSS var back (a
fast single-frame flick could be lost to a cancelled rAF, and a
pre-rehydration read returned '' -> NaN), floor the panel/terminal max
clamp at the minimum so narrow viewports can't invert the clamp, guard
pointerup/pointercancel by pointerId so a second touch pointer can't
kill the drag, and capture the terminal rect once on drag start instead
of per-frame getBoundingClientRect. Remove the now-dead isResizing store
state and centralize CONTENT_WINDOW_GAP in stores/constants.
* fix(workflow): compute drag value rAF-aligned from the latest pointer event
Run compute inside the rAF (before the CSS-var write, so any layout read
hits clean layout at most once per frame) and derive the final value from
the latest pointer event on release. The output-panel hook now captures
the terminal element on drag start and re-reads its rect per frame, so
the clamp stays correct when the terminal resizes mid-drag and the live
width can never exceed the current max.
* fix(terminal): clamp output panel against the live CSS-var width
The ResizeObserver clamp compared the persisted store width, which is
intentionally stale during a drag; a terminal shrink mid-drag could
overwrite the live width with a stale store value. Compare against the
live --output-panel-width variable (store as pre-write fallback) so the
clamp converges with the drag instead of fighting it.
* feat(landing): X pixel conversion tracking on landing pages
* fix(landing): dedupe X pixel initial PageView by URL to survive Strict Mode effect replay
* fix(landing): scope X pixel URL dedupe to the tracker instance so same-URL returns to landing still track
* feat(storage): native Google Cloud Storage support for self-hosting
Adds GCS as a third object-storage backend with full parity with S3 and
Azure Blob: uploads, streaming downloads, deletes, head, V4 signed URLs
(single + batch), and browser/server multipart uploads via the GCS XML
API. Selection precedence is Azure Blob > S3 > GCS > local disk.
- new provider client at lib/uploads/providers/gcs (cached singleton,
ADC/Workload Identity or inline GCS_CREDENTIALS_JSON auth)
- per-context GCS_*_BUCKET_NAME config wired through getStorageConfig
- shared getServeStoragePrefix() replaces hardcoded blob/s3 serve paths
- docs (object-storage, environment-variables), .env.example, helm
values.yaml + values-gcp.yaml storage section
* fix(storage): review round 1 — GCS per-context bucket fallback + ETag quote normalization
- getGcsConfig falls back to the general bucket for every context (GCS bucket
names are globally unique, so the S3-style sim-execution-files literal default
would point at an unowned bucket; empty per-context buckets previously made
uploads and downloads disagree)
- completeGcsMultipartUpload restores quotes on ETags stripped by the shared
browser upload client before building the completion XML
- docs/.env.example/helm updated for the fallback behavior
* fix(storage): review round 2 — route chat authz and execution-URL detection through getStorageConfig
- getChatStorageConfig delegates to getStorageConfig('chat') (identical for
S3/Azure, picks up the GCS general-bucket fallback instead of reading the
raw chat config and rejecting valid chat files)
- parse route resolves the execution bucket via getStorageConfig('execution')
for all providers, so GCS execution files in the fallback bucket are still
recognized as our own objects
* fix(storage): validation pass — gcs serve-prefix parity in key parsers + CORS doc fix
- extractStorageKey, extractFilename, and extractEmbeddedFileRef now strip the
gcs/ serve prefix like s3/ and blob/, so direct-uploaded files on GCS parse,
delete, download, and embed correctly (previously only the serve route knew
the prefix)
- file-download storageProvider union includes 'gcs'
- completeGcsMultipartUpload defensively rejects a 200 response carrying an
XML error document
- docs: CORS example lists concrete x-goog-meta-* header names (GCS matches
responseHeader entries exactly; wildcards are only supported for origin)
* improvement(checkout): enforce team/enterprise-only org subscriptions, block double-covered personal checkouts, and drop renewal-triggered workspace detach
* close race with personal pro / org inclusions
* address comments
* fix(billing): compute org coverage independently of the personal-sub lookup and fail closed on unverifiable plan writes