| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
CMUX Vault: cloud sync CLI, multi-tenant backend, dashboard (#7324) * Add CMUX Vault cloud sync: Go CLI + web backend for coding-agent transcripts cmux-vault (new top-level vault/ Go module) discovers Claude Code, Codex, and pi session transcripts on disk and syncs them to multi-tenant cloud storage. Subcommands: login, logout, scan, sync, resume, status, version. Sync is incremental (size+mtime fast path, sha256 confirm) with streamed zstd compression and direct-to-storage presigned PUTs. resume restores a locally-deleted session from the cloud to the exact path its agent expects and prints the agent resume command. Backend in web/: three drizzle tables (vault_sessions, vault_snapshots, vault_cli_auth_requests) with committed migration; S3-compatible presign service (optional endpoint, so R2 works); routes under /api/vault for upload presigning, commit-after-HeadObject verification, listing, and download. Object keys are always derived server-side under vault/u/<userId>/ and every query is user-scoped. CLI auth is a device-code flow: the CLI polls while the user approves on a signed-in page; tokens are minted with the same createSession primitive as the native macOS sign-in, stored hashed-code-only, and claimed exactly once inside a FOR UPDATE transaction. Localized (en/ja) approval page and sessions dashboard, no useEffect. Discovery handles real-world layouts: UUIDv7 session ids (codex/pi), symlinked agent roots (shared claude stores), unreadable dirs and files deleted mid-scan (skip and warn). Verified against a real machine: 8815 codex / 601 claude / 86 pi sessions discovered read-only. vault/DESIGN.md records cadence, metered-network, security/retention, quota, and OpenCode/Gemini follow-up decisions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mint CLI auth tokens at claim time, never store them Review (Greptile) flagged that approve stored 90-day refresh tokens as plaintext JSONB until the CLI claimed them, leaving them readable in the DB and persisted in WAL/backups, and that minting before the pending-row guard could orphan a Stack session on duplicate approves. Approval now only records the approving userId with a single guarded UPDATE; the poll route mints the session at claim time (StackServerApp.getUser(id) + createSession) after winning the FOR UPDATE claim transaction. The tokens column is gone from vault_cli_auth_requests (migration regenerated; it had never shipped), a user_code index backs the approve lookup, and a mint failure restores the approval so the next poll retries within the 15-minute expiry window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Cursor/Codex/Greptile review findings Approve now targets exactly one pending request (select oldest by created_at, then guarded update by id) since user codes are random but not unique, and all three CLI auth routes sit behind the isVaultConfigured 503 gate like the data routes. The start route opportunistically deletes rows expired for over a minute so the unauthenticated endpoint cannot accumulate state beyond one expiry window. Commit only enforces the size check when HEAD reports a Content-Length. resume --force skips the local fast path so the cloud copy actually replaces a corrupt local transcript, FindSession requests two rows and errors when the id exists under multiple agents instead of restoring an arbitrary one, and resume hints only emit cd for absolute cwd values (never the lossy munged-directory fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add vault dashboard: (dashboard) route group, auth shell, virtualized sessions Consolidates the remaining loose marketing routes (homepage, docs, blog, community, ios, nightly, wall-of-love, download/confirmation, deeplink, assets) into the existing (landing) route group and moves SiteFooter out of the locale root layout into the (landing)/(legal) group layouts, so each group owns its chrome; URLs are unchanged and moved files switch to the @/ import alias. New (dashboard) group: layout gates Stack auth (redirect to sign-in with return URL), wraps StackProvider/StackTheme, and renders a sidebar shell (Overview, Sessions, CLI setup) with UserButton. Vault pages move inside it. /vault shows per-agent aggregates from one grouped query. The sessions list is a virtualized client table (@tanstack/react-virtual) with infinite cursor loading from the authenticated JSON API, agent filter tabs, debounced search (ref timer, cancelled on filter change and row navigation), and richer columns (agent badge, copyable id, cwd with basename+path, raw/compressed sizes, snapshot count, first/last upload). Search stays fast at scale via a pg_trgm migration with GIN trigram indexes on cwd/rel_path; the list API gains snapshotCount and agentSessionId-prefix matching while staying backward compatible with the Go CLI. New detail page shows metadata, snapshots, a presigned download, a copyable resume command, and a transcript preview that streams the object through fzstd with 8 MiB decompressed / 32 MiB compressed caps and tolerant per-agent JSONL parsing (unit-tested for claude, codex, and pi line shapes). Transcript-content search is deferred to an upload-time indexing pipeline (noted in code); metadata search ships now. All new strings localized in en and ja; no useEffect in new code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move dashboard under a literal /dashboard URL prefix Vault dashboard routes move from /vault/* to /dashboard/vault/*, keeping the root URL namespace free for marketing (/docs/vault already exists) and giving future authed surfaces one home under /dashboard. With a real path segment the (dashboard) route-group parens were redundant, so the group directory becomes app/[locale]/dashboard/ with the same layout; (landing) and (legal) stay as groups because their URLs must remain unprefixed. The device-code verificationUrl and all in-app links now point at /dashboard/vault/cli-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restyle dashboard: square corners, monochrome, purposefully plain Dashboard-only visual pass: every rounded-* class removed, no shadows or gradients, monochrome token palette only (agent badges are now bordered uppercase mono labels with no per-agent color), two font sizes total (text-sm default, text-xs for dense data), structure drawn with 1px border-border lines instead of filled cards, all data values monospace, controls are transparent bordered squares with full-invert hover and a square focus-visible outline. No functional, i18n, or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard feedback: no uppercase, one unified list, transcript first Removes all uppercase/tracking styling, deletes the agent filter tabs so sessions are a single stream (agent stays as a small mono label; the API keeps its agent param for the CLI), collapses the overview per-agent cards into one totals strip with a muted inline count line, and inverts the detail page: slim cwd/agent/resume header, transcript preview as the dominant 65vh element, metadata and snapshots demoted to native details blocks below. Unused message keys removed, new ones added in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transcript view: full-page layout, all messages, RSC-streamed head batch Detail page becomes a full-page transcript: messages fill the content area left-aligned at readable width with a floating top-right metadata panel (id/cwd/sizes/dates, resume command, download, snapshots in a collapsible details), back link top-left. All messages now render, not a capped preview. A new authenticated pass-through route streams the compressed object same-origin (no bucket CORS needed, no buffering); the client decompresses zstd with fzstd in the browser, parses JSONL incrementally across chunk boundaries, and virtualizes rows with dynamic measurement, appending in 500-message batches with a 256 MiB safety valve. For no extra hop on first paint, an async server component inside Suspense parses a 500-message/2 MiB head batch directly from storage during the page render; the RSC stream carries it with the page and useVirtualizer initialRect renders the first viewport in SSR HTML. The client continuation skips the deterministic prefix (invariant pinned by a determinism test) and only runs when the transcript did not fit. Judge round fixed one dev-fatal bug: React 19 StrictMode replays callback refs, which aborted the continuation and left startedRef stuck; detach now re-arms the guard and a restarted load resets to the server batch so replays cannot duplicate rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the dashboard multi-product: vault and subrouter sections The sidebar now has vault and subrouter product groups, the shell brand is product-neutral cmux linking to a new /dashboard launcher index (one bordered box per product), and /dashboard/subrouter is a structural stub with a localized description and coming-soon empty state so the real subrouter integration can fill in pages later. Nav strings move to a dashboard.nav namespace in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard shell: plain sidebar links, theme toggle, fix 1px overflow Sidebar nav items lose their bordered boxes: plain text links, muted when inactive, foreground when active. The top bar gains a dark/light toggle; its label renders via CSS visibility (dark:hidden vs dark:inline) so SSR never depends on the client theme, and the handler reads resolvedTheme only on click (no useEffect). The persistent scrollbar on short pages came from the header being h-11 content plus a 1px border while the grid reserved 100vh minus 2.75rem; the height now sits on the bordered element so border-box absorbs the pixel and the layout sums to exactly 100vh. Toggle labels localized in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review-bot feedback: sync integrity, auth return URL, per-item upload errors vault CLI: - hash transcripts while compressing so the committed sha256 always matches the uploaded snapshot, even if the agent keeps writing during sync - sync --dry-run no longer saves state.json - blob HTTP client gets a 15-minute timeout so stalled S3 transfers cannot hang the CLI forever - resume: lowercase UUID-shaped session ids, atomic no-overwrite restore via os.Link (closes the stat/rename TOCTOU), and a clearer error when --force finds a local transcript but no vault copy - login --json routes the approval prompt to stderr, keeping stdout parseable - bump klauspost/compress to v1.18.6 web: - dashboard auth redirects move from the layout into each page so /dashboard/vault/cli-auth?code=... survives sign-in - uploads + commit routes return per-item upload_too_large instead of failing the whole batch; unchanged uploads refresh relPath/cwd so moved transcripts restore to the right place - session detail degrades to downloadUrl: null when presign fails - wall-of-love header section label is localized * Address structured review: throttle auth start, chunked transcript state, cached formatters, keyboard rows - /api/vault/cli/auth/start caps concurrently pending device-code rows at 200 and returns 429 beyond it, bounding unauthenticated DB growth - transcript viewer stores messages in append-only chunks so each streaming flush copies the chunk list, not every loaded message - vault list formatters (Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat) are cached per locale instead of allocated per cell render - session rows are focusable and open on Enter/Space Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault auth start: per-IP firewall throttle, count only pending rows The global cap previously counted every unexpired row regardless of status, so 200 cheap unauthenticated POSTs (or even 200 completed logins) inside one 15-minute window blocked all further CLI logins. Now the primary control is the per-IP Vercel firewall rate limit (same pattern as the waitlist and feedback endpoints), and the global backstop counts only rows still pending approval, so completed logins never consume capacity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: enforce per-user storage quota at presign and commit Add CMUX_VAULT_MAX_USER_BYTES (default 50 GiB compressed) so an authenticated account cannot grow object storage without bound. The uploads route checks the projected per-user total before minting each presigned PUT and the commit route re-checks it, so previously issued URLs cannot bypass the cap. Failures are per-item (quota_exceeded), matching the existing upload_too_large flow. DESIGN.md quota section updated to match the enforced behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: count pending upload grants against quota, GC orphaned objects Presigned PUT URLs previously escaped the per-user quota: a client could mint URLs with arbitrary sha256 values, upload, and never commit, growing the bucket with objects the committed-bytes sum never sees. Every minted URL now records a vault_upload_grants row (the signed Content-Length bounds the real upload), unexpired grants count against CMUX_VAULT_MAX_USER_BYTES at presign time, commit releases the grant in the same transaction, and expired uncommitted grants plus their orphaned storage objects are garbage-collected opportunistically by the uploads route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard: reuse the site ThemeToggle, sharpen sidebar group labels Replaces the dashboard's bespoke text theme button with the marketing site's shared ThemeToggle (sun/moon icons, view-transition animation, theme-color meta sync), dropping the now-unused toggle message keys. Sidebar product-group labels become 11px semibold foreground so they read as headings above the muted nav items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Patch @stackframe/stack SsrScript for React 19 script-tag warning React 19 warns when a component renders an inline <script> ("Scripts inside React components are never executed when rendering on the client"), which StackTheme's BrowserScript does on every dashboard render. The script element only matters for the SSR HTML (pre-hydration theme sync); the client path is already covered by the component's useLayoutEffect eval. The bun patch moves the SSR copy out of the React tree via useServerInsertedHTML and returns null, eliminating the warning with identical behavior in both dist variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument vault API routes: spans, hardened errors, storage logging Every /api/vault/* handler now runs inside withApiRouteSpan via shared vault route wrappers mirroring the VM route pattern: route-named spans with safe attributes (authed user id, agent filter, item/result counts, byte totals, outcome markers) and never transcript content, paths, codes, tokens, or presigned URLs. Unexpected errors record a span error, log with a stable route prefix, and return 500 internal_error instead of leaking details. Storage and quota-ledger failures log operation name and object key; the transcript head-batch fetch logs failures while keeping its graceful fallback. CMUX_VAULT_MAX_USER_BYTES documented in .env.example; wrapper behavior unit-tested (auth failure, sanitized unexpected error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix order-dependent 401 assertion in vault route helper test CI runs every bun test file in one process, and several suites mock.module app/lib/stack with a fake signed-in user; depending on file order the vault wrapper test's real verifyRequest then resolved a user and the 401 assertion saw 200 (passed locally where fewer files ran). withAuthedVaultApiRoute now takes an injectable verifier defaulting to the real verifyRequest, and the test pins the unauthenticated outcome explicitly. Production call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
Fix Codex Security scan findings (#7437) * Add security regression coverage * Fix security scan findings * Address security review follow-ups * Fix review regressions in lease cleanup * Fix final security review findings * Fix remaining autoreview findings * Address final autoreview regressions * Keep active identity cleanup best effort * Bound active identity cleanup * Bound SSH cleanup before endpoint minting * Make cleanup retries bounded and releasable * Bound active identity cleanup preflight * Preserve vault grants on retry presign failure * Guard vault grant rollback state * Back off failed expired lease cleanup * Separate vault quota lock namespace * Tighten VM identity cleanup ordering * Fail closed without VM team membership * Bound VM identity cleanup fanout * Rollback endpoint resume on cleanup failure * Remove nondeterministic vault upload test wait * Fail closed on destroy identity cleanup * Recreate Base when active provider VM is gone * Keep Freestyle attach independent of exec probe * Scope provider identity not-found handling * Use reservation tokens for vault upload rollback * Validate vault upload grants at commit * Bound identity cleanup and duplicate vault reservations * Stage vault uploads before commit * Keep vault staging cleanup retryable * Reuse active vault upload staging keys * Preserve legacy vault upload commits * Make vault staging cleanup recoverable * Avoid endpoint resume rollback races * Serialize vault upload grant cleanup * Finalize vault staging outside quota locks * Track superseded vault upload keys | 2 个月前 | |
CMUX Vault: cloud sync CLI, multi-tenant backend, dashboard (#7324) * Add CMUX Vault cloud sync: Go CLI + web backend for coding-agent transcripts cmux-vault (new top-level vault/ Go module) discovers Claude Code, Codex, and pi session transcripts on disk and syncs them to multi-tenant cloud storage. Subcommands: login, logout, scan, sync, resume, status, version. Sync is incremental (size+mtime fast path, sha256 confirm) with streamed zstd compression and direct-to-storage presigned PUTs. resume restores a locally-deleted session from the cloud to the exact path its agent expects and prints the agent resume command. Backend in web/: three drizzle tables (vault_sessions, vault_snapshots, vault_cli_auth_requests) with committed migration; S3-compatible presign service (optional endpoint, so R2 works); routes under /api/vault for upload presigning, commit-after-HeadObject verification, listing, and download. Object keys are always derived server-side under vault/u/<userId>/ and every query is user-scoped. CLI auth is a device-code flow: the CLI polls while the user approves on a signed-in page; tokens are minted with the same createSession primitive as the native macOS sign-in, stored hashed-code-only, and claimed exactly once inside a FOR UPDATE transaction. Localized (en/ja) approval page and sessions dashboard, no useEffect. Discovery handles real-world layouts: UUIDv7 session ids (codex/pi), symlinked agent roots (shared claude stores), unreadable dirs and files deleted mid-scan (skip and warn). Verified against a real machine: 8815 codex / 601 claude / 86 pi sessions discovered read-only. vault/DESIGN.md records cadence, metered-network, security/retention, quota, and OpenCode/Gemini follow-up decisions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mint CLI auth tokens at claim time, never store them Review (Greptile) flagged that approve stored 90-day refresh tokens as plaintext JSONB until the CLI claimed them, leaving them readable in the DB and persisted in WAL/backups, and that minting before the pending-row guard could orphan a Stack session on duplicate approves. Approval now only records the approving userId with a single guarded UPDATE; the poll route mints the session at claim time (StackServerApp.getUser(id) + createSession) after winning the FOR UPDATE claim transaction. The tokens column is gone from vault_cli_auth_requests (migration regenerated; it had never shipped), a user_code index backs the approve lookup, and a mint failure restores the approval so the next poll retries within the 15-minute expiry window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Cursor/Codex/Greptile review findings Approve now targets exactly one pending request (select oldest by created_at, then guarded update by id) since user codes are random but not unique, and all three CLI auth routes sit behind the isVaultConfigured 503 gate like the data routes. The start route opportunistically deletes rows expired for over a minute so the unauthenticated endpoint cannot accumulate state beyond one expiry window. Commit only enforces the size check when HEAD reports a Content-Length. resume --force skips the local fast path so the cloud copy actually replaces a corrupt local transcript, FindSession requests two rows and errors when the id exists under multiple agents instead of restoring an arbitrary one, and resume hints only emit cd for absolute cwd values (never the lossy munged-directory fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add vault dashboard: (dashboard) route group, auth shell, virtualized sessions Consolidates the remaining loose marketing routes (homepage, docs, blog, community, ios, nightly, wall-of-love, download/confirmation, deeplink, assets) into the existing (landing) route group and moves SiteFooter out of the locale root layout into the (landing)/(legal) group layouts, so each group owns its chrome; URLs are unchanged and moved files switch to the @/ import alias. New (dashboard) group: layout gates Stack auth (redirect to sign-in with return URL), wraps StackProvider/StackTheme, and renders a sidebar shell (Overview, Sessions, CLI setup) with UserButton. Vault pages move inside it. /vault shows per-agent aggregates from one grouped query. The sessions list is a virtualized client table (@tanstack/react-virtual) with infinite cursor loading from the authenticated JSON API, agent filter tabs, debounced search (ref timer, cancelled on filter change and row navigation), and richer columns (agent badge, copyable id, cwd with basename+path, raw/compressed sizes, snapshot count, first/last upload). Search stays fast at scale via a pg_trgm migration with GIN trigram indexes on cwd/rel_path; the list API gains snapshotCount and agentSessionId-prefix matching while staying backward compatible with the Go CLI. New detail page shows metadata, snapshots, a presigned download, a copyable resume command, and a transcript preview that streams the object through fzstd with 8 MiB decompressed / 32 MiB compressed caps and tolerant per-agent JSONL parsing (unit-tested for claude, codex, and pi line shapes). Transcript-content search is deferred to an upload-time indexing pipeline (noted in code); metadata search ships now. All new strings localized in en and ja; no useEffect in new code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move dashboard under a literal /dashboard URL prefix Vault dashboard routes move from /vault/* to /dashboard/vault/*, keeping the root URL namespace free for marketing (/docs/vault already exists) and giving future authed surfaces one home under /dashboard. With a real path segment the (dashboard) route-group parens were redundant, so the group directory becomes app/[locale]/dashboard/ with the same layout; (landing) and (legal) stay as groups because their URLs must remain unprefixed. The device-code verificationUrl and all in-app links now point at /dashboard/vault/cli-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restyle dashboard: square corners, monochrome, purposefully plain Dashboard-only visual pass: every rounded-* class removed, no shadows or gradients, monochrome token palette only (agent badges are now bordered uppercase mono labels with no per-agent color), two font sizes total (text-sm default, text-xs for dense data), structure drawn with 1px border-border lines instead of filled cards, all data values monospace, controls are transparent bordered squares with full-invert hover and a square focus-visible outline. No functional, i18n, or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard feedback: no uppercase, one unified list, transcript first Removes all uppercase/tracking styling, deletes the agent filter tabs so sessions are a single stream (agent stays as a small mono label; the API keeps its agent param for the CLI), collapses the overview per-agent cards into one totals strip with a muted inline count line, and inverts the detail page: slim cwd/agent/resume header, transcript preview as the dominant 65vh element, metadata and snapshots demoted to native details blocks below. Unused message keys removed, new ones added in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transcript view: full-page layout, all messages, RSC-streamed head batch Detail page becomes a full-page transcript: messages fill the content area left-aligned at readable width with a floating top-right metadata panel (id/cwd/sizes/dates, resume command, download, snapshots in a collapsible details), back link top-left. All messages now render, not a capped preview. A new authenticated pass-through route streams the compressed object same-origin (no bucket CORS needed, no buffering); the client decompresses zstd with fzstd in the browser, parses JSONL incrementally across chunk boundaries, and virtualizes rows with dynamic measurement, appending in 500-message batches with a 256 MiB safety valve. For no extra hop on first paint, an async server component inside Suspense parses a 500-message/2 MiB head batch directly from storage during the page render; the RSC stream carries it with the page and useVirtualizer initialRect renders the first viewport in SSR HTML. The client continuation skips the deterministic prefix (invariant pinned by a determinism test) and only runs when the transcript did not fit. Judge round fixed one dev-fatal bug: React 19 StrictMode replays callback refs, which aborted the continuation and left startedRef stuck; detach now re-arms the guard and a restarted load resets to the server batch so replays cannot duplicate rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the dashboard multi-product: vault and subrouter sections The sidebar now has vault and subrouter product groups, the shell brand is product-neutral cmux linking to a new /dashboard launcher index (one bordered box per product), and /dashboard/subrouter is a structural stub with a localized description and coming-soon empty state so the real subrouter integration can fill in pages later. Nav strings move to a dashboard.nav namespace in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard shell: plain sidebar links, theme toggle, fix 1px overflow Sidebar nav items lose their bordered boxes: plain text links, muted when inactive, foreground when active. The top bar gains a dark/light toggle; its label renders via CSS visibility (dark:hidden vs dark:inline) so SSR never depends on the client theme, and the handler reads resolvedTheme only on click (no useEffect). The persistent scrollbar on short pages came from the header being h-11 content plus a 1px border while the grid reserved 100vh minus 2.75rem; the height now sits on the bordered element so border-box absorbs the pixel and the layout sums to exactly 100vh. Toggle labels localized in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review-bot feedback: sync integrity, auth return URL, per-item upload errors vault CLI: - hash transcripts while compressing so the committed sha256 always matches the uploaded snapshot, even if the agent keeps writing during sync - sync --dry-run no longer saves state.json - blob HTTP client gets a 15-minute timeout so stalled S3 transfers cannot hang the CLI forever - resume: lowercase UUID-shaped session ids, atomic no-overwrite restore via os.Link (closes the stat/rename TOCTOU), and a clearer error when --force finds a local transcript but no vault copy - login --json routes the approval prompt to stderr, keeping stdout parseable - bump klauspost/compress to v1.18.6 web: - dashboard auth redirects move from the layout into each page so /dashboard/vault/cli-auth?code=... survives sign-in - uploads + commit routes return per-item upload_too_large instead of failing the whole batch; unchanged uploads refresh relPath/cwd so moved transcripts restore to the right place - session detail degrades to downloadUrl: null when presign fails - wall-of-love header section label is localized * Address structured review: throttle auth start, chunked transcript state, cached formatters, keyboard rows - /api/vault/cli/auth/start caps concurrently pending device-code rows at 200 and returns 429 beyond it, bounding unauthenticated DB growth - transcript viewer stores messages in append-only chunks so each streaming flush copies the chunk list, not every loaded message - vault list formatters (Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat) are cached per locale instead of allocated per cell render - session rows are focusable and open on Enter/Space Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault auth start: per-IP firewall throttle, count only pending rows The global cap previously counted every unexpired row regardless of status, so 200 cheap unauthenticated POSTs (or even 200 completed logins) inside one 15-minute window blocked all further CLI logins. Now the primary control is the per-IP Vercel firewall rate limit (same pattern as the waitlist and feedback endpoints), and the global backstop counts only rows still pending approval, so completed logins never consume capacity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: enforce per-user storage quota at presign and commit Add CMUX_VAULT_MAX_USER_BYTES (default 50 GiB compressed) so an authenticated account cannot grow object storage without bound. The uploads route checks the projected per-user total before minting each presigned PUT and the commit route re-checks it, so previously issued URLs cannot bypass the cap. Failures are per-item (quota_exceeded), matching the existing upload_too_large flow. DESIGN.md quota section updated to match the enforced behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: count pending upload grants against quota, GC orphaned objects Presigned PUT URLs previously escaped the per-user quota: a client could mint URLs with arbitrary sha256 values, upload, and never commit, growing the bucket with objects the committed-bytes sum never sees. Every minted URL now records a vault_upload_grants row (the signed Content-Length bounds the real upload), unexpired grants count against CMUX_VAULT_MAX_USER_BYTES at presign time, commit releases the grant in the same transaction, and expired uncommitted grants plus their orphaned storage objects are garbage-collected opportunistically by the uploads route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard: reuse the site ThemeToggle, sharpen sidebar group labels Replaces the dashboard's bespoke text theme button with the marketing site's shared ThemeToggle (sun/moon icons, view-transition animation, theme-color meta sync), dropping the now-unused toggle message keys. Sidebar product-group labels become 11px semibold foreground so they read as headings above the muted nav items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Patch @stackframe/stack SsrScript for React 19 script-tag warning React 19 warns when a component renders an inline <script> ("Scripts inside React components are never executed when rendering on the client"), which StackTheme's BrowserScript does on every dashboard render. The script element only matters for the SSR HTML (pre-hydration theme sync); the client path is already covered by the component's useLayoutEffect eval. The bun patch moves the SSR copy out of the React tree via useServerInsertedHTML and returns null, eliminating the warning with identical behavior in both dist variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument vault API routes: spans, hardened errors, storage logging Every /api/vault/* handler now runs inside withApiRouteSpan via shared vault route wrappers mirroring the VM route pattern: route-named spans with safe attributes (authed user id, agent filter, item/result counts, byte totals, outcome markers) and never transcript content, paths, codes, tokens, or presigned URLs. Unexpected errors record a span error, log with a stable route prefix, and return 500 internal_error instead of leaking details. Storage and quota-ledger failures log operation name and object key; the transcript head-batch fetch logs failures while keeping its graceful fallback. CMUX_VAULT_MAX_USER_BYTES documented in .env.example; wrapper behavior unit-tested (auth failure, sanitized unexpected error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix order-dependent 401 assertion in vault route helper test CI runs every bun test file in one process, and several suites mock.module app/lib/stack with a fake signed-in user; depending on file order the vault wrapper test's real verifyRequest then resolved a user and the 401 assertion saw 200 (passed locally where fewer files ran). withAuthedVaultApiRoute now takes an injectable verifier defaulting to the real verifyRequest, and the test pins the unauthenticated outcome explicitly. Production call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
CMUX Vault: cloud sync CLI, multi-tenant backend, dashboard (#7324) * Add CMUX Vault cloud sync: Go CLI + web backend for coding-agent transcripts cmux-vault (new top-level vault/ Go module) discovers Claude Code, Codex, and pi session transcripts on disk and syncs them to multi-tenant cloud storage. Subcommands: login, logout, scan, sync, resume, status, version. Sync is incremental (size+mtime fast path, sha256 confirm) with streamed zstd compression and direct-to-storage presigned PUTs. resume restores a locally-deleted session from the cloud to the exact path its agent expects and prints the agent resume command. Backend in web/: three drizzle tables (vault_sessions, vault_snapshots, vault_cli_auth_requests) with committed migration; S3-compatible presign service (optional endpoint, so R2 works); routes under /api/vault for upload presigning, commit-after-HeadObject verification, listing, and download. Object keys are always derived server-side under vault/u/<userId>/ and every query is user-scoped. CLI auth is a device-code flow: the CLI polls while the user approves on a signed-in page; tokens are minted with the same createSession primitive as the native macOS sign-in, stored hashed-code-only, and claimed exactly once inside a FOR UPDATE transaction. Localized (en/ja) approval page and sessions dashboard, no useEffect. Discovery handles real-world layouts: UUIDv7 session ids (codex/pi), symlinked agent roots (shared claude stores), unreadable dirs and files deleted mid-scan (skip and warn). Verified against a real machine: 8815 codex / 601 claude / 86 pi sessions discovered read-only. vault/DESIGN.md records cadence, metered-network, security/retention, quota, and OpenCode/Gemini follow-up decisions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mint CLI auth tokens at claim time, never store them Review (Greptile) flagged that approve stored 90-day refresh tokens as plaintext JSONB until the CLI claimed them, leaving them readable in the DB and persisted in WAL/backups, and that minting before the pending-row guard could orphan a Stack session on duplicate approves. Approval now only records the approving userId with a single guarded UPDATE; the poll route mints the session at claim time (StackServerApp.getUser(id) + createSession) after winning the FOR UPDATE claim transaction. The tokens column is gone from vault_cli_auth_requests (migration regenerated; it had never shipped), a user_code index backs the approve lookup, and a mint failure restores the approval so the next poll retries within the 15-minute expiry window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Cursor/Codex/Greptile review findings Approve now targets exactly one pending request (select oldest by created_at, then guarded update by id) since user codes are random but not unique, and all three CLI auth routes sit behind the isVaultConfigured 503 gate like the data routes. The start route opportunistically deletes rows expired for over a minute so the unauthenticated endpoint cannot accumulate state beyond one expiry window. Commit only enforces the size check when HEAD reports a Content-Length. resume --force skips the local fast path so the cloud copy actually replaces a corrupt local transcript, FindSession requests two rows and errors when the id exists under multiple agents instead of restoring an arbitrary one, and resume hints only emit cd for absolute cwd values (never the lossy munged-directory fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add vault dashboard: (dashboard) route group, auth shell, virtualized sessions Consolidates the remaining loose marketing routes (homepage, docs, blog, community, ios, nightly, wall-of-love, download/confirmation, deeplink, assets) into the existing (landing) route group and moves SiteFooter out of the locale root layout into the (landing)/(legal) group layouts, so each group owns its chrome; URLs are unchanged and moved files switch to the @/ import alias. New (dashboard) group: layout gates Stack auth (redirect to sign-in with return URL), wraps StackProvider/StackTheme, and renders a sidebar shell (Overview, Sessions, CLI setup) with UserButton. Vault pages move inside it. /vault shows per-agent aggregates from one grouped query. The sessions list is a virtualized client table (@tanstack/react-virtual) with infinite cursor loading from the authenticated JSON API, agent filter tabs, debounced search (ref timer, cancelled on filter change and row navigation), and richer columns (agent badge, copyable id, cwd with basename+path, raw/compressed sizes, snapshot count, first/last upload). Search stays fast at scale via a pg_trgm migration with GIN trigram indexes on cwd/rel_path; the list API gains snapshotCount and agentSessionId-prefix matching while staying backward compatible with the Go CLI. New detail page shows metadata, snapshots, a presigned download, a copyable resume command, and a transcript preview that streams the object through fzstd with 8 MiB decompressed / 32 MiB compressed caps and tolerant per-agent JSONL parsing (unit-tested for claude, codex, and pi line shapes). Transcript-content search is deferred to an upload-time indexing pipeline (noted in code); metadata search ships now. All new strings localized in en and ja; no useEffect in new code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move dashboard under a literal /dashboard URL prefix Vault dashboard routes move from /vault/* to /dashboard/vault/*, keeping the root URL namespace free for marketing (/docs/vault already exists) and giving future authed surfaces one home under /dashboard. With a real path segment the (dashboard) route-group parens were redundant, so the group directory becomes app/[locale]/dashboard/ with the same layout; (landing) and (legal) stay as groups because their URLs must remain unprefixed. The device-code verificationUrl and all in-app links now point at /dashboard/vault/cli-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restyle dashboard: square corners, monochrome, purposefully plain Dashboard-only visual pass: every rounded-* class removed, no shadows or gradients, monochrome token palette only (agent badges are now bordered uppercase mono labels with no per-agent color), two font sizes total (text-sm default, text-xs for dense data), structure drawn with 1px border-border lines instead of filled cards, all data values monospace, controls are transparent bordered squares with full-invert hover and a square focus-visible outline. No functional, i18n, or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard feedback: no uppercase, one unified list, transcript first Removes all uppercase/tracking styling, deletes the agent filter tabs so sessions are a single stream (agent stays as a small mono label; the API keeps its agent param for the CLI), collapses the overview per-agent cards into one totals strip with a muted inline count line, and inverts the detail page: slim cwd/agent/resume header, transcript preview as the dominant 65vh element, metadata and snapshots demoted to native details blocks below. Unused message keys removed, new ones added in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transcript view: full-page layout, all messages, RSC-streamed head batch Detail page becomes a full-page transcript: messages fill the content area left-aligned at readable width with a floating top-right metadata panel (id/cwd/sizes/dates, resume command, download, snapshots in a collapsible details), back link top-left. All messages now render, not a capped preview. A new authenticated pass-through route streams the compressed object same-origin (no bucket CORS needed, no buffering); the client decompresses zstd with fzstd in the browser, parses JSONL incrementally across chunk boundaries, and virtualizes rows with dynamic measurement, appending in 500-message batches with a 256 MiB safety valve. For no extra hop on first paint, an async server component inside Suspense parses a 500-message/2 MiB head batch directly from storage during the page render; the RSC stream carries it with the page and useVirtualizer initialRect renders the first viewport in SSR HTML. The client continuation skips the deterministic prefix (invariant pinned by a determinism test) and only runs when the transcript did not fit. Judge round fixed one dev-fatal bug: React 19 StrictMode replays callback refs, which aborted the continuation and left startedRef stuck; detach now re-arms the guard and a restarted load resets to the server batch so replays cannot duplicate rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the dashboard multi-product: vault and subrouter sections The sidebar now has vault and subrouter product groups, the shell brand is product-neutral cmux linking to a new /dashboard launcher index (one bordered box per product), and /dashboard/subrouter is a structural stub with a localized description and coming-soon empty state so the real subrouter integration can fill in pages later. Nav strings move to a dashboard.nav namespace in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard shell: plain sidebar links, theme toggle, fix 1px overflow Sidebar nav items lose their bordered boxes: plain text links, muted when inactive, foreground when active. The top bar gains a dark/light toggle; its label renders via CSS visibility (dark:hidden vs dark:inline) so SSR never depends on the client theme, and the handler reads resolvedTheme only on click (no useEffect). The persistent scrollbar on short pages came from the header being h-11 content plus a 1px border while the grid reserved 100vh minus 2.75rem; the height now sits on the bordered element so border-box absorbs the pixel and the layout sums to exactly 100vh. Toggle labels localized in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review-bot feedback: sync integrity, auth return URL, per-item upload errors vault CLI: - hash transcripts while compressing so the committed sha256 always matches the uploaded snapshot, even if the agent keeps writing during sync - sync --dry-run no longer saves state.json - blob HTTP client gets a 15-minute timeout so stalled S3 transfers cannot hang the CLI forever - resume: lowercase UUID-shaped session ids, atomic no-overwrite restore via os.Link (closes the stat/rename TOCTOU), and a clearer error when --force finds a local transcript but no vault copy - login --json routes the approval prompt to stderr, keeping stdout parseable - bump klauspost/compress to v1.18.6 web: - dashboard auth redirects move from the layout into each page so /dashboard/vault/cli-auth?code=... survives sign-in - uploads + commit routes return per-item upload_too_large instead of failing the whole batch; unchanged uploads refresh relPath/cwd so moved transcripts restore to the right place - session detail degrades to downloadUrl: null when presign fails - wall-of-love header section label is localized * Address structured review: throttle auth start, chunked transcript state, cached formatters, keyboard rows - /api/vault/cli/auth/start caps concurrently pending device-code rows at 200 and returns 429 beyond it, bounding unauthenticated DB growth - transcript viewer stores messages in append-only chunks so each streaming flush copies the chunk list, not every loaded message - vault list formatters (Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat) are cached per locale instead of allocated per cell render - session rows are focusable and open on Enter/Space Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault auth start: per-IP firewall throttle, count only pending rows The global cap previously counted every unexpired row regardless of status, so 200 cheap unauthenticated POSTs (or even 200 completed logins) inside one 15-minute window blocked all further CLI logins. Now the primary control is the per-IP Vercel firewall rate limit (same pattern as the waitlist and feedback endpoints), and the global backstop counts only rows still pending approval, so completed logins never consume capacity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: enforce per-user storage quota at presign and commit Add CMUX_VAULT_MAX_USER_BYTES (default 50 GiB compressed) so an authenticated account cannot grow object storage without bound. The uploads route checks the projected per-user total before minting each presigned PUT and the commit route re-checks it, so previously issued URLs cannot bypass the cap. Failures are per-item (quota_exceeded), matching the existing upload_too_large flow. DESIGN.md quota section updated to match the enforced behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: count pending upload grants against quota, GC orphaned objects Presigned PUT URLs previously escaped the per-user quota: a client could mint URLs with arbitrary sha256 values, upload, and never commit, growing the bucket with objects the committed-bytes sum never sees. Every minted URL now records a vault_upload_grants row (the signed Content-Length bounds the real upload), unexpired grants count against CMUX_VAULT_MAX_USER_BYTES at presign time, commit releases the grant in the same transaction, and expired uncommitted grants plus their orphaned storage objects are garbage-collected opportunistically by the uploads route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard: reuse the site ThemeToggle, sharpen sidebar group labels Replaces the dashboard's bespoke text theme button with the marketing site's shared ThemeToggle (sun/moon icons, view-transition animation, theme-color meta sync), dropping the now-unused toggle message keys. Sidebar product-group labels become 11px semibold foreground so they read as headings above the muted nav items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Patch @stackframe/stack SsrScript for React 19 script-tag warning React 19 warns when a component renders an inline <script> ("Scripts inside React components are never executed when rendering on the client"), which StackTheme's BrowserScript does on every dashboard render. The script element only matters for the SSR HTML (pre-hydration theme sync); the client path is already covered by the component's useLayoutEffect eval. The bun patch moves the SSR copy out of the React tree via useServerInsertedHTML and returns null, eliminating the warning with identical behavior in both dist variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument vault API routes: spans, hardened errors, storage logging Every /api/vault/* handler now runs inside withApiRouteSpan via shared vault route wrappers mirroring the VM route pattern: route-named spans with safe attributes (authed user id, agent filter, item/result counts, byte totals, outcome markers) and never transcript content, paths, codes, tokens, or presigned URLs. Unexpected errors record a span error, log with a stable route prefix, and return 500 internal_error instead of leaking details. Storage and quota-ledger failures log operation name and object key; the transcript head-batch fetch logs failures while keeping its graceful fallback. CMUX_VAULT_MAX_USER_BYTES documented in .env.example; wrapper behavior unit-tested (auth failure, sanitized unexpected error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix order-dependent 401 assertion in vault route helper test CI runs every bun test file in one process, and several suites mock.module app/lib/stack with a fake signed-in user; depending on file order the vault wrapper test's real verifyRequest then resolved a user and the 401 assertion saw 200 (passed locally where fewer files ran). withAuthedVaultApiRoute now takes an injectable verifier defaulting to the real verifyRequest, and the test pins the unauthenticated outcome explicitly. Production call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
CMUX Vault: cloud sync CLI, multi-tenant backend, dashboard (#7324) * Add CMUX Vault cloud sync: Go CLI + web backend for coding-agent transcripts cmux-vault (new top-level vault/ Go module) discovers Claude Code, Codex, and pi session transcripts on disk and syncs them to multi-tenant cloud storage. Subcommands: login, logout, scan, sync, resume, status, version. Sync is incremental (size+mtime fast path, sha256 confirm) with streamed zstd compression and direct-to-storage presigned PUTs. resume restores a locally-deleted session from the cloud to the exact path its agent expects and prints the agent resume command. Backend in web/: three drizzle tables (vault_sessions, vault_snapshots, vault_cli_auth_requests) with committed migration; S3-compatible presign service (optional endpoint, so R2 works); routes under /api/vault for upload presigning, commit-after-HeadObject verification, listing, and download. Object keys are always derived server-side under vault/u/<userId>/ and every query is user-scoped. CLI auth is a device-code flow: the CLI polls while the user approves on a signed-in page; tokens are minted with the same createSession primitive as the native macOS sign-in, stored hashed-code-only, and claimed exactly once inside a FOR UPDATE transaction. Localized (en/ja) approval page and sessions dashboard, no useEffect. Discovery handles real-world layouts: UUIDv7 session ids (codex/pi), symlinked agent roots (shared claude stores), unreadable dirs and files deleted mid-scan (skip and warn). Verified against a real machine: 8815 codex / 601 claude / 86 pi sessions discovered read-only. vault/DESIGN.md records cadence, metered-network, security/retention, quota, and OpenCode/Gemini follow-up decisions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mint CLI auth tokens at claim time, never store them Review (Greptile) flagged that approve stored 90-day refresh tokens as plaintext JSONB until the CLI claimed them, leaving them readable in the DB and persisted in WAL/backups, and that minting before the pending-row guard could orphan a Stack session on duplicate approves. Approval now only records the approving userId with a single guarded UPDATE; the poll route mints the session at claim time (StackServerApp.getUser(id) + createSession) after winning the FOR UPDATE claim transaction. The tokens column is gone from vault_cli_auth_requests (migration regenerated; it had never shipped), a user_code index backs the approve lookup, and a mint failure restores the approval so the next poll retries within the 15-minute expiry window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Cursor/Codex/Greptile review findings Approve now targets exactly one pending request (select oldest by created_at, then guarded update by id) since user codes are random but not unique, and all three CLI auth routes sit behind the isVaultConfigured 503 gate like the data routes. The start route opportunistically deletes rows expired for over a minute so the unauthenticated endpoint cannot accumulate state beyond one expiry window. Commit only enforces the size check when HEAD reports a Content-Length. resume --force skips the local fast path so the cloud copy actually replaces a corrupt local transcript, FindSession requests two rows and errors when the id exists under multiple agents instead of restoring an arbitrary one, and resume hints only emit cd for absolute cwd values (never the lossy munged-directory fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add vault dashboard: (dashboard) route group, auth shell, virtualized sessions Consolidates the remaining loose marketing routes (homepage, docs, blog, community, ios, nightly, wall-of-love, download/confirmation, deeplink, assets) into the existing (landing) route group and moves SiteFooter out of the locale root layout into the (landing)/(legal) group layouts, so each group owns its chrome; URLs are unchanged and moved files switch to the @/ import alias. New (dashboard) group: layout gates Stack auth (redirect to sign-in with return URL), wraps StackProvider/StackTheme, and renders a sidebar shell (Overview, Sessions, CLI setup) with UserButton. Vault pages move inside it. /vault shows per-agent aggregates from one grouped query. The sessions list is a virtualized client table (@tanstack/react-virtual) with infinite cursor loading from the authenticated JSON API, agent filter tabs, debounced search (ref timer, cancelled on filter change and row navigation), and richer columns (agent badge, copyable id, cwd with basename+path, raw/compressed sizes, snapshot count, first/last upload). Search stays fast at scale via a pg_trgm migration with GIN trigram indexes on cwd/rel_path; the list API gains snapshotCount and agentSessionId-prefix matching while staying backward compatible with the Go CLI. New detail page shows metadata, snapshots, a presigned download, a copyable resume command, and a transcript preview that streams the object through fzstd with 8 MiB decompressed / 32 MiB compressed caps and tolerant per-agent JSONL parsing (unit-tested for claude, codex, and pi line shapes). Transcript-content search is deferred to an upload-time indexing pipeline (noted in code); metadata search ships now. All new strings localized in en and ja; no useEffect in new code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move dashboard under a literal /dashboard URL prefix Vault dashboard routes move from /vault/* to /dashboard/vault/*, keeping the root URL namespace free for marketing (/docs/vault already exists) and giving future authed surfaces one home under /dashboard. With a real path segment the (dashboard) route-group parens were redundant, so the group directory becomes app/[locale]/dashboard/ with the same layout; (landing) and (legal) stay as groups because their URLs must remain unprefixed. The device-code verificationUrl and all in-app links now point at /dashboard/vault/cli-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restyle dashboard: square corners, monochrome, purposefully plain Dashboard-only visual pass: every rounded-* class removed, no shadows or gradients, monochrome token palette only (agent badges are now bordered uppercase mono labels with no per-agent color), two font sizes total (text-sm default, text-xs for dense data), structure drawn with 1px border-border lines instead of filled cards, all data values monospace, controls are transparent bordered squares with full-invert hover and a square focus-visible outline. No functional, i18n, or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard feedback: no uppercase, one unified list, transcript first Removes all uppercase/tracking styling, deletes the agent filter tabs so sessions are a single stream (agent stays as a small mono label; the API keeps its agent param for the CLI), collapses the overview per-agent cards into one totals strip with a muted inline count line, and inverts the detail page: slim cwd/agent/resume header, transcript preview as the dominant 65vh element, metadata and snapshots demoted to native details blocks below. Unused message keys removed, new ones added in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transcript view: full-page layout, all messages, RSC-streamed head batch Detail page becomes a full-page transcript: messages fill the content area left-aligned at readable width with a floating top-right metadata panel (id/cwd/sizes/dates, resume command, download, snapshots in a collapsible details), back link top-left. All messages now render, not a capped preview. A new authenticated pass-through route streams the compressed object same-origin (no bucket CORS needed, no buffering); the client decompresses zstd with fzstd in the browser, parses JSONL incrementally across chunk boundaries, and virtualizes rows with dynamic measurement, appending in 500-message batches with a 256 MiB safety valve. For no extra hop on first paint, an async server component inside Suspense parses a 500-message/2 MiB head batch directly from storage during the page render; the RSC stream carries it with the page and useVirtualizer initialRect renders the first viewport in SSR HTML. The client continuation skips the deterministic prefix (invariant pinned by a determinism test) and only runs when the transcript did not fit. Judge round fixed one dev-fatal bug: React 19 StrictMode replays callback refs, which aborted the continuation and left startedRef stuck; detach now re-arms the guard and a restarted load resets to the server batch so replays cannot duplicate rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the dashboard multi-product: vault and subrouter sections The sidebar now has vault and subrouter product groups, the shell brand is product-neutral cmux linking to a new /dashboard launcher index (one bordered box per product), and /dashboard/subrouter is a structural stub with a localized description and coming-soon empty state so the real subrouter integration can fill in pages later. Nav strings move to a dashboard.nav namespace in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard shell: plain sidebar links, theme toggle, fix 1px overflow Sidebar nav items lose their bordered boxes: plain text links, muted when inactive, foreground when active. The top bar gains a dark/light toggle; its label renders via CSS visibility (dark:hidden vs dark:inline) so SSR never depends on the client theme, and the handler reads resolvedTheme only on click (no useEffect). The persistent scrollbar on short pages came from the header being h-11 content plus a 1px border while the grid reserved 100vh minus 2.75rem; the height now sits on the bordered element so border-box absorbs the pixel and the layout sums to exactly 100vh. Toggle labels localized in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review-bot feedback: sync integrity, auth return URL, per-item upload errors vault CLI: - hash transcripts while compressing so the committed sha256 always matches the uploaded snapshot, even if the agent keeps writing during sync - sync --dry-run no longer saves state.json - blob HTTP client gets a 15-minute timeout so stalled S3 transfers cannot hang the CLI forever - resume: lowercase UUID-shaped session ids, atomic no-overwrite restore via os.Link (closes the stat/rename TOCTOU), and a clearer error when --force finds a local transcript but no vault copy - login --json routes the approval prompt to stderr, keeping stdout parseable - bump klauspost/compress to v1.18.6 web: - dashboard auth redirects move from the layout into each page so /dashboard/vault/cli-auth?code=... survives sign-in - uploads + commit routes return per-item upload_too_large instead of failing the whole batch; unchanged uploads refresh relPath/cwd so moved transcripts restore to the right place - session detail degrades to downloadUrl: null when presign fails - wall-of-love header section label is localized * Address structured review: throttle auth start, chunked transcript state, cached formatters, keyboard rows - /api/vault/cli/auth/start caps concurrently pending device-code rows at 200 and returns 429 beyond it, bounding unauthenticated DB growth - transcript viewer stores messages in append-only chunks so each streaming flush copies the chunk list, not every loaded message - vault list formatters (Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat) are cached per locale instead of allocated per cell render - session rows are focusable and open on Enter/Space Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault auth start: per-IP firewall throttle, count only pending rows The global cap previously counted every unexpired row regardless of status, so 200 cheap unauthenticated POSTs (or even 200 completed logins) inside one 15-minute window blocked all further CLI logins. Now the primary control is the per-IP Vercel firewall rate limit (same pattern as the waitlist and feedback endpoints), and the global backstop counts only rows still pending approval, so completed logins never consume capacity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: enforce per-user storage quota at presign and commit Add CMUX_VAULT_MAX_USER_BYTES (default 50 GiB compressed) so an authenticated account cannot grow object storage without bound. The uploads route checks the projected per-user total before minting each presigned PUT and the commit route re-checks it, so previously issued URLs cannot bypass the cap. Failures are per-item (quota_exceeded), matching the existing upload_too_large flow. DESIGN.md quota section updated to match the enforced behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: count pending upload grants against quota, GC orphaned objects Presigned PUT URLs previously escaped the per-user quota: a client could mint URLs with arbitrary sha256 values, upload, and never commit, growing the bucket with objects the committed-bytes sum never sees. Every minted URL now records a vault_upload_grants row (the signed Content-Length bounds the real upload), unexpired grants count against CMUX_VAULT_MAX_USER_BYTES at presign time, commit releases the grant in the same transaction, and expired uncommitted grants plus their orphaned storage objects are garbage-collected opportunistically by the uploads route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard: reuse the site ThemeToggle, sharpen sidebar group labels Replaces the dashboard's bespoke text theme button with the marketing site's shared ThemeToggle (sun/moon icons, view-transition animation, theme-color meta sync), dropping the now-unused toggle message keys. Sidebar product-group labels become 11px semibold foreground so they read as headings above the muted nav items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Patch @stackframe/stack SsrScript for React 19 script-tag warning React 19 warns when a component renders an inline <script> ("Scripts inside React components are never executed when rendering on the client"), which StackTheme's BrowserScript does on every dashboard render. The script element only matters for the SSR HTML (pre-hydration theme sync); the client path is already covered by the component's useLayoutEffect eval. The bun patch moves the SSR copy out of the React tree via useServerInsertedHTML and returns null, eliminating the warning with identical behavior in both dist variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument vault API routes: spans, hardened errors, storage logging Every /api/vault/* handler now runs inside withApiRouteSpan via shared vault route wrappers mirroring the VM route pattern: route-named spans with safe attributes (authed user id, agent filter, item/result counts, byte totals, outcome markers) and never transcript content, paths, codes, tokens, or presigned URLs. Unexpected errors record a span error, log with a stable route prefix, and return 500 internal_error instead of leaking details. Storage and quota-ledger failures log operation name and object key; the transcript head-batch fetch logs failures while keeping its graceful fallback. CMUX_VAULT_MAX_USER_BYTES documented in .env.example; wrapper behavior unit-tested (auth failure, sanitized unexpected error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix order-dependent 401 assertion in vault route helper test CI runs every bun test file in one process, and several suites mock.module app/lib/stack with a fake signed-in user; depending on file order the vault wrapper test's real verifyRequest then resolved a user and the 401 assertion saw 200 (passed locally where fewer files ran). withAuthedVaultApiRoute now takes an injectable verifier defaulting to the real verifyRequest, and the test pins the unauthenticated outcome explicitly. Production call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
CMUX Vault: cloud sync CLI, multi-tenant backend, dashboard (#7324) * Add CMUX Vault cloud sync: Go CLI + web backend for coding-agent transcripts cmux-vault (new top-level vault/ Go module) discovers Claude Code, Codex, and pi session transcripts on disk and syncs them to multi-tenant cloud storage. Subcommands: login, logout, scan, sync, resume, status, version. Sync is incremental (size+mtime fast path, sha256 confirm) with streamed zstd compression and direct-to-storage presigned PUTs. resume restores a locally-deleted session from the cloud to the exact path its agent expects and prints the agent resume command. Backend in web/: three drizzle tables (vault_sessions, vault_snapshots, vault_cli_auth_requests) with committed migration; S3-compatible presign service (optional endpoint, so R2 works); routes under /api/vault for upload presigning, commit-after-HeadObject verification, listing, and download. Object keys are always derived server-side under vault/u/<userId>/ and every query is user-scoped. CLI auth is a device-code flow: the CLI polls while the user approves on a signed-in page; tokens are minted with the same createSession primitive as the native macOS sign-in, stored hashed-code-only, and claimed exactly once inside a FOR UPDATE transaction. Localized (en/ja) approval page and sessions dashboard, no useEffect. Discovery handles real-world layouts: UUIDv7 session ids (codex/pi), symlinked agent roots (shared claude stores), unreadable dirs and files deleted mid-scan (skip and warn). Verified against a real machine: 8815 codex / 601 claude / 86 pi sessions discovered read-only. vault/DESIGN.md records cadence, metered-network, security/retention, quota, and OpenCode/Gemini follow-up decisions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mint CLI auth tokens at claim time, never store them Review (Greptile) flagged that approve stored 90-day refresh tokens as plaintext JSONB until the CLI claimed them, leaving them readable in the DB and persisted in WAL/backups, and that minting before the pending-row guard could orphan a Stack session on duplicate approves. Approval now only records the approving userId with a single guarded UPDATE; the poll route mints the session at claim time (StackServerApp.getUser(id) + createSession) after winning the FOR UPDATE claim transaction. The tokens column is gone from vault_cli_auth_requests (migration regenerated; it had never shipped), a user_code index backs the approve lookup, and a mint failure restores the approval so the next poll retries within the 15-minute expiry window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Cursor/Codex/Greptile review findings Approve now targets exactly one pending request (select oldest by created_at, then guarded update by id) since user codes are random but not unique, and all three CLI auth routes sit behind the isVaultConfigured 503 gate like the data routes. The start route opportunistically deletes rows expired for over a minute so the unauthenticated endpoint cannot accumulate state beyond one expiry window. Commit only enforces the size check when HEAD reports a Content-Length. resume --force skips the local fast path so the cloud copy actually replaces a corrupt local transcript, FindSession requests two rows and errors when the id exists under multiple agents instead of restoring an arbitrary one, and resume hints only emit cd for absolute cwd values (never the lossy munged-directory fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add vault dashboard: (dashboard) route group, auth shell, virtualized sessions Consolidates the remaining loose marketing routes (homepage, docs, blog, community, ios, nightly, wall-of-love, download/confirmation, deeplink, assets) into the existing (landing) route group and moves SiteFooter out of the locale root layout into the (landing)/(legal) group layouts, so each group owns its chrome; URLs are unchanged and moved files switch to the @/ import alias. New (dashboard) group: layout gates Stack auth (redirect to sign-in with return URL), wraps StackProvider/StackTheme, and renders a sidebar shell (Overview, Sessions, CLI setup) with UserButton. Vault pages move inside it. /vault shows per-agent aggregates from one grouped query. The sessions list is a virtualized client table (@tanstack/react-virtual) with infinite cursor loading from the authenticated JSON API, agent filter tabs, debounced search (ref timer, cancelled on filter change and row navigation), and richer columns (agent badge, copyable id, cwd with basename+path, raw/compressed sizes, snapshot count, first/last upload). Search stays fast at scale via a pg_trgm migration with GIN trigram indexes on cwd/rel_path; the list API gains snapshotCount and agentSessionId-prefix matching while staying backward compatible with the Go CLI. New detail page shows metadata, snapshots, a presigned download, a copyable resume command, and a transcript preview that streams the object through fzstd with 8 MiB decompressed / 32 MiB compressed caps and tolerant per-agent JSONL parsing (unit-tested for claude, codex, and pi line shapes). Transcript-content search is deferred to an upload-time indexing pipeline (noted in code); metadata search ships now. All new strings localized in en and ja; no useEffect in new code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move dashboard under a literal /dashboard URL prefix Vault dashboard routes move from /vault/* to /dashboard/vault/*, keeping the root URL namespace free for marketing (/docs/vault already exists) and giving future authed surfaces one home under /dashboard. With a real path segment the (dashboard) route-group parens were redundant, so the group directory becomes app/[locale]/dashboard/ with the same layout; (landing) and (legal) stay as groups because their URLs must remain unprefixed. The device-code verificationUrl and all in-app links now point at /dashboard/vault/cli-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restyle dashboard: square corners, monochrome, purposefully plain Dashboard-only visual pass: every rounded-* class removed, no shadows or gradients, monochrome token palette only (agent badges are now bordered uppercase mono labels with no per-agent color), two font sizes total (text-sm default, text-xs for dense data), structure drawn with 1px border-border lines instead of filled cards, all data values monospace, controls are transparent bordered squares with full-invert hover and a square focus-visible outline. No functional, i18n, or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard feedback: no uppercase, one unified list, transcript first Removes all uppercase/tracking styling, deletes the agent filter tabs so sessions are a single stream (agent stays as a small mono label; the API keeps its agent param for the CLI), collapses the overview per-agent cards into one totals strip with a muted inline count line, and inverts the detail page: slim cwd/agent/resume header, transcript preview as the dominant 65vh element, metadata and snapshots demoted to native details blocks below. Unused message keys removed, new ones added in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transcript view: full-page layout, all messages, RSC-streamed head batch Detail page becomes a full-page transcript: messages fill the content area left-aligned at readable width with a floating top-right metadata panel (id/cwd/sizes/dates, resume command, download, snapshots in a collapsible details), back link top-left. All messages now render, not a capped preview. A new authenticated pass-through route streams the compressed object same-origin (no bucket CORS needed, no buffering); the client decompresses zstd with fzstd in the browser, parses JSONL incrementally across chunk boundaries, and virtualizes rows with dynamic measurement, appending in 500-message batches with a 256 MiB safety valve. For no extra hop on first paint, an async server component inside Suspense parses a 500-message/2 MiB head batch directly from storage during the page render; the RSC stream carries it with the page and useVirtualizer initialRect renders the first viewport in SSR HTML. The client continuation skips the deterministic prefix (invariant pinned by a determinism test) and only runs when the transcript did not fit. Judge round fixed one dev-fatal bug: React 19 StrictMode replays callback refs, which aborted the continuation and left startedRef stuck; detach now re-arms the guard and a restarted load resets to the server batch so replays cannot duplicate rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the dashboard multi-product: vault and subrouter sections The sidebar now has vault and subrouter product groups, the shell brand is product-neutral cmux linking to a new /dashboard launcher index (one bordered box per product), and /dashboard/subrouter is a structural stub with a localized description and coming-soon empty state so the real subrouter integration can fill in pages later. Nav strings move to a dashboard.nav namespace in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard shell: plain sidebar links, theme toggle, fix 1px overflow Sidebar nav items lose their bordered boxes: plain text links, muted when inactive, foreground when active. The top bar gains a dark/light toggle; its label renders via CSS visibility (dark:hidden vs dark:inline) so SSR never depends on the client theme, and the handler reads resolvedTheme only on click (no useEffect). The persistent scrollbar on short pages came from the header being h-11 content plus a 1px border while the grid reserved 100vh minus 2.75rem; the height now sits on the bordered element so border-box absorbs the pixel and the layout sums to exactly 100vh. Toggle labels localized in en and ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review-bot feedback: sync integrity, auth return URL, per-item upload errors vault CLI: - hash transcripts while compressing so the committed sha256 always matches the uploaded snapshot, even if the agent keeps writing during sync - sync --dry-run no longer saves state.json - blob HTTP client gets a 15-minute timeout so stalled S3 transfers cannot hang the CLI forever - resume: lowercase UUID-shaped session ids, atomic no-overwrite restore via os.Link (closes the stat/rename TOCTOU), and a clearer error when --force finds a local transcript but no vault copy - login --json routes the approval prompt to stderr, keeping stdout parseable - bump klauspost/compress to v1.18.6 web: - dashboard auth redirects move from the layout into each page so /dashboard/vault/cli-auth?code=... survives sign-in - uploads + commit routes return per-item upload_too_large instead of failing the whole batch; unchanged uploads refresh relPath/cwd so moved transcripts restore to the right place - session detail degrades to downloadUrl: null when presign fails - wall-of-love header section label is localized * Address structured review: throttle auth start, chunked transcript state, cached formatters, keyboard rows - /api/vault/cli/auth/start caps concurrently pending device-code rows at 200 and returns 429 beyond it, bounding unauthenticated DB growth - transcript viewer stores messages in append-only chunks so each streaming flush copies the chunk list, not every loaded message - vault list formatters (Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat) are cached per locale instead of allocated per cell render - session rows are focusable and open on Enter/Space Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault auth start: per-IP firewall throttle, count only pending rows The global cap previously counted every unexpired row regardless of status, so 200 cheap unauthenticated POSTs (or even 200 completed logins) inside one 15-minute window blocked all further CLI logins. Now the primary control is the per-IP Vercel firewall rate limit (same pattern as the waitlist and feedback endpoints), and the global backstop counts only rows still pending approval, so completed logins never consume capacity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: enforce per-user storage quota at presign and commit Add CMUX_VAULT_MAX_USER_BYTES (default 50 GiB compressed) so an authenticated account cannot grow object storage without bound. The uploads route checks the projected per-user total before minting each presigned PUT and the commit route re-checks it, so previously issued URLs cannot bypass the cap. Failures are per-item (quota_exceeded), matching the existing upload_too_large flow. DESIGN.md quota section updated to match the enforced behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vault: count pending upload grants against quota, GC orphaned objects Presigned PUT URLs previously escaped the per-user quota: a client could mint URLs with arbitrary sha256 values, upload, and never commit, growing the bucket with objects the committed-bytes sum never sees. Every minted URL now records a vault_upload_grants row (the signed Content-Length bounds the real upload), unexpired grants count against CMUX_VAULT_MAX_USER_BYTES at presign time, commit releases the grant in the same transaction, and expired uncommitted grants plus their orphaned storage objects are garbage-collected opportunistically by the uploads route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Dashboard: reuse the site ThemeToggle, sharpen sidebar group labels Replaces the dashboard's bespoke text theme button with the marketing site's shared ThemeToggle (sun/moon icons, view-transition animation, theme-color meta sync), dropping the now-unused toggle message keys. Sidebar product-group labels become 11px semibold foreground so they read as headings above the muted nav items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Patch @stackframe/stack SsrScript for React 19 script-tag warning React 19 warns when a component renders an inline <script> ("Scripts inside React components are never executed when rendering on the client"), which StackTheme's BrowserScript does on every dashboard render. The script element only matters for the SSR HTML (pre-hydration theme sync); the client path is already covered by the component's useLayoutEffect eval. The bun patch moves the SSR copy out of the React tree via useServerInsertedHTML and returns null, eliminating the warning with identical behavior in both dist variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Instrument vault API routes: spans, hardened errors, storage logging Every /api/vault/* handler now runs inside withApiRouteSpan via shared vault route wrappers mirroring the VM route pattern: route-named spans with safe attributes (authed user id, agent filter, item/result counts, byte totals, outcome markers) and never transcript content, paths, codes, tokens, or presigned URLs. Unexpected errors record a span error, log with a stable route prefix, and return 500 internal_error instead of leaking details. Storage and quota-ledger failures log operation name and object key; the transcript head-batch fetch logs failures while keeping its graceful fallback. CMUX_VAULT_MAX_USER_BYTES documented in .env.example; wrapper behavior unit-tested (auth failure, sanitized unexpected error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix order-dependent 401 assertion in vault route helper test CI runs every bun test file in one process, and several suites mock.module app/lib/stack with a fake signed-in user; depending on file order the vault wrapper test's real verifyRequest then resolved a user and the 401 assertion saw 200 (passed locally where fewer files ran). withAuthedVaultApiRoute now takes an injectable verifier defaulting to the real verifyRequest, and the test pins the unauthenticated outcome explicitly. Production call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 |
cmux-vault
cmux-vault discovers local coding-agent session transcripts and syncs them to
cmux Vault cloud storage. Round 1 supports Claude Code, Codex, and pi.
Install
go build ./cmd/cmux-vault
Commands
cmux-vault login
cmux-vault scan
cmux-vault sync
cmux-vault resume <session-id>
cmux-vault status
cmux-vault logout
login starts a device-code flow, prints a verification URL and user code, and
stores Stack Auth tokens in ~/.config/cmux-vault/auth.json with mode 0600.
sync uploads changed transcripts directly to S3-compatible object storage via
presigned URLs. resume restores a missing transcript from cloud storage and
prints the command the agent expects.
Useful flags:
cmux-vault --json scan
cmux-vault sync --agent codex --dry-run
cmux-vault sync --limit 25
cmux-vault resume --agent claude <session-id>
cmux-vault resume --force <session-id>
Environment
CMUX_VAULT_API_BASE: web API base URL. Defaults tohttps://cmux.com.CMUX_VAULT_CONFIG_DIR: override the auth token directory.CMUX_VAULT_STATE_DIR: override the sync state directory.CLAUDE_CONFIG_DIR: override Claude Code config discovery.CODEX_HOME: override Codex discovery.
Default local state lives in ~/.local/state/cmux-vault/state.json.