Skip to content

feat(chat): support .zip uploads with extract into workspace files/#5564

Closed
Sg312 wants to merge 21 commits into
devfrom
feat/zip-extract
Closed

feat(chat): support .zip uploads with extract into workspace files/#5564
Sg312 wants to merge 21 commits into
devfrom
feat/zip-extract

Conversation

@Sg312

@Sg312 Sg312 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds .zip support to copilot chat uploads with an explicit extract-first flow: a zip is stored once in the chat's uploads/ namespace, and the agent unpacks it with materialize_file(operation: "extract") into a files/<archive>/ folder tree, where the contents are readable with the normal files/ tooling. Reads/greps of a raw zip return extract-first guidance instead of binary bytes. The shared extraction primitives (zip-bomb / zip-slip / symlink guards, size caps) are factored out of the file-manage decompress route into lib/uploads/archive.ts and reused by both paths.

The branch also hardens the extract path from a deep review pass: the zip-bomb pre-scan now walks the real EOCD-anchored central directory (reusing file-parsers/zip-guard) so zips containing stored nested archives are no longer falsely rejected; extraction is all-or-nothing (a discarding validation pass proves the caps before anything is uploaded, so lying headers or corrupt entries can't leave partial trees); save/import refuse archives (saving stranded the contents unreachably); re-extracting into a non-empty folder is refused instead of duplicating every file as a (1).txt; degenerate names (..zip, control chars) fold into a safe fallback folder; small mislabeled .zip files are byte-sniffed so they stay readable; and zip acceptance is scoped to the mothership flow only (execution/workspace/deployed-chat upload gates keep rejecting zips up front). ArchiveError messages are single-sourced with accurate caps, and per-entry zip-slip forensics are logged again.

Type of Change

  • New feature

Testing

  • 153 unit tests across archive (incl. new regression tests: nested stored-zip false positive, corrupt-entry all-or-nothing, noise entries not counting toward the 1000-file cap, synthetic central-directory cap tests), materialize-file (save-on-zip guard, already-extracted guard, fileNames dedupe, degenerate-name fallback, cross-workspace guard), upload-file-reader (zip guidance, large-zip no-download, mislabeled-zip readable), zip-guard, validation, file-utils, payload, and the upload/presigned routes
  • tsc, biome, and check:api-validation all clean
  • Generated tool-catalog-v1.ts / tool-schemas-v1.ts regenerated from the companion's contract and byte-verified in sync
  • Reviewers should focus on: the two-pass extraction loop in lib/uploads/archive.ts, the gate scoping in lib/uploads/utils/validation.ts + upload/presigned routes, and the extract guards in lib/copilot/tools/handlers/materialize-file.ts
  • Known deferred follow-ups (called out for visibility): no extract path for zips already under files/ (compress-op output, nested zips), and the per-entry sequential upload loop / whole-archive buffering inherited from the old decompress route

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Companion PR

Companion: https://github.com/simstudioai/mothership/pull/348

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 21, 2026 12:46am

Request Review

@Sg312 Sg312 changed the title fix(zip-uploads): harden extract path from review findings feat(chat): support .zip uploads with extract into workspace files/ Jul 10, 2026
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds zip upload support to the chat file flow. The main changes are:

  • Zip files can be uploaded through the mothership chat path.
  • Archive reads and greps now point the agent to an explicit extract step.
  • materialize_file can extract zip contents into workspace files.
  • Shared archive validation and extraction helpers are reused by the file manage route.

Confidence Score: 4/5

This is close, but the failed-extract cleanup should be fixed before merging.

  • A mid-extraction upload failure can still leave storage usage charged for hidden files.
  • The duplicate extraction guards and small mislabeled zip handling look addressed in the changed paths.

apps/sim/lib/uploads/archive.ts

Important Files Changed

Filename Overview
apps/sim/lib/uploads/archive.ts Adds the shared two-pass archive extraction path, with cleanup that still misses storage usage rollback on failed uploads.
apps/sim/lib/copilot/tools/handlers/materialize-file.ts Adds archive extraction, workspace ownership checks, repeated-name dedupe, and existing destination guards.
apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts Adds extract-first handling for real zip uploads and byte sniffing for small mislabeled zip names.
apps/sim/lib/uploads/utils/validation.ts Scopes archive acceptance to the mothership upload flow and exposes the matching file input accept string.

Reviews (3): Last reviewed commit: "fix(zip-uploads): detect prior nested-on..." | Re-trigger Greptile

Comment thread apps/sim/lib/uploads/archive.ts Outdated
Comment thread apps/sim/lib/copilot/tools/handlers/materialize-file.ts
Comment thread apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts
@Sg312

Sg312 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/copilot/tools/handlers/materialize-file.ts Outdated
@Sg312

Sg312 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

Comment on lines +374 to +376
for (const file of extracted) {
try {
await deleteWorkspaceFile(workspaceId, file.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rollback Leaves Quota This rollback hides files that were uploaded before a later archive entry fails, but uploadWorkspaceFile has already charged storage usage for each successful upload. deleteWorkspaceFile only sets deletedAt, so a failed extraction can leave the workspace quota consumed by files the caller can no longer see or clean up. The cleanup path needs to reverse the storage/accounting side effects too, not just hide the metadata.

waleedlatif1 and others added 10 commits July 19, 2026 01:25
…5760)

* 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
#5762)

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.
… can't strand the connect (#5767)

* 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.
* 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>
…te to the agent (#5656)

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.
…clobber (#5772)

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.
…emplate title (#5775)

* 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.
waleedlatif1 and others added 11 commits July 20, 2026 14:51
* 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.
* 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>
…ds (#5783)

* fix(library): show full unambiguous dates on featured and related cards

* fix(library): format content dates in UTC to avoid off-by-one day
#5781)

* 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
…nsport (#5782)

* 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.
…5785)

* 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
- 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
- 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
…act 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.
@Sg312
Sg312 force-pushed the feat/zip-extract branch from 9935ec9 to 06d7aaa Compare July 21, 2026 00:46
@waleedlatif1
waleedlatif1 deleted the feat/zip-extract branch July 21, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants