Repository navigation
Add cmux browser repl: a Playwright-shaped browser REPL for agents - #15570
lawrencecchen wants to merge 398 commits into
Conversation
|
Important Review skippedToo many files! This PR contains 388 files, which is 88 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configuration
📒 Files selected for processing (388)
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
All contributors have signed the CLA ✍️ ✅ |
CI failure attributionCI passes on Written by |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
1 similar comment
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Dogfood tours of
|
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
60 issues found across 535 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:44">
P1: The sandbox authorizes a path and then uses it in separate filesystem calls, leaving a symlink-swap TOCTOU gap. Use descriptor-relative operations with no-followed directory descriptors, or revalidate the opened file before exposing data or mutating it.
(Based on your team's feedback about descriptor-relative filesystem operations.)</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:59">
P2: `writeFile` silently treats missing or invalid base64 as an empty file and reports success. Reject malformed payloads with `EINVAL` before opening or truncating the destination.</violation>
<violation number="3" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:105">
P1: `rm` follows an in-sandbox symlink because it operates on the canonical target returned by `path(.write)`. Preserve the lexical final component and unlink the symlink itself after authorizing the resolved target.</violation>
<violation number="4" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:134">
P0: `rename` and `copyFile` remove an existing destination before the source operation succeeds. A missing source or `from == to` can destroy data, and `to == "."` bypasses `rm`’s root guard; reject root endpoints and use source-first atomic replacement instead.</violation>
</file>
<file name="Resources/browser-repl/sites/linear.js">
<violation number="1" location="Resources/browser-repl/sites/linear.js:64">
P1: This guard only rejects documents whose first token is `mutation`; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:210">
P1: A close can queue cleanup before an already-submitted `beginEval`; `beginEval` does not re-check `closed`, so it can start a new evaluation after cleanup and leave its continuation unresolved when the stop block runs. Reject the state on the JS thread before proceeding with context setup.</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:238">
P1: This timeout cannot interrupt synchronous JavaScript because it queues `finishEval` on the thread currently executing `evalFunction.call(...)`. A cell such as `while (true) {}` therefore hangs the evaluation and blocks `close()` as well; use an interruptible JavaScript execution/watchdog path.</violation>
<violation number="3" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:239">
P1: The timeout only completes the Swift continuation; it leaves the JavaScript `__cmuxReplEval` promise running. Because `repl-host.js` serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.</violation>
<violation number="4" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:358">
P1: `setTimer` can crash the app for a finite delay outside `Int64` range, such as `setTimeout(() => {}, 1e20)`. Clamp the delay to the supported timer range before converting it to `Int64`.</violation>
</file>
<file name="Resources/browser-repl/sites/gmail.js">
<violation number="1" location="Resources/browser-repl/sites/gmail.js:44">
P2: `readThreadBody` treats any message-body link containing `view=att` as an attachment. Restrict discovery to Gmail attachment containers so crafted or ordinary body links cannot be listed and fetched as attachments.</violation>
<violation number="2" location="Resources/browser-repl/sites/gmail.js:144">
P1: `finishSend` searches the whole document for `Message sent`, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.</violation>
</file>
<file name="Resources/browser-repl/sites/browser-auth.js">
<violation number="1" location="Resources/browser-repl/sites/browser-auth.js:46">
P2: This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict `HTMLInputElement` to the supported text-like types (or require the DOM type to match `f.type`) before marking the element for credential fill.</violation>
<violation number="2" location="Resources/browser-repl/sites/browser-auth.js:50">
P1: `browserAuth` validates only the top-level origin, so a locator in a cross-origin iframe can receive the credentials while the sheet names the parent page. Reject fields whose holder frame has a different origin, or pass the holder origin through the native request and verify it before filling.</violation>
</file>
<file name="Resources/browser-repl/sites/slack.js">
<violation number="1" location="Resources/browser-repl/sites/slack.js:26">
P1: This request trusts `team.url` from localStorage and can post the workspace token to any origin. Validate the URL as an HTTPS Slack host before appending the token and fetching, so malformed or tampered workspace metadata cannot exfiltrate credentials.</violation>
<violation number="2" location="Resources/browser-repl/sites/slack.js:117">
P1: A draft without `team` can post to a different workspace than the one active when the user reviewed it: changing Slack's active workspace before confirmation changes `lastActiveTeamId`, and line 117 resolves it again. Bind the concrete workspace (and channel ID) when creating the draft, or require an explicit stable workspace ID for posts.</violation>
</file>
<file name="tests/browser-parity/compare/chatgpt-live.ts">
<violation number="1" location="tests/browser-parity/compare/chatgpt-live.ts:195">
P1: Parity cleanup closes every tab returned by the current-workspace list, including tabs that existed before the run. Track the IDs opened by this script and close only those IDs.</violation>
</file>
<file name="Resources/browser-repl/sites/webmcp.js">
<violation number="1" location="Resources/browser-repl/sites/webmcp.js:61">
P2: This makes a valid page tool named like `draft-1-abcdef` impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.
(Based on your team's feedback about user-representable string sentinels.)</violation>
<violation number="2" location="Resources/browser-repl/sites/webmcp.js:66">
P1: This trusts an untrusted page's `readOnlyHint` as the confirmation boundary, so a mutating WebMCP tool can mark itself read-only and send or change data without a draft. Treat the annotation as informational and require host-side classification or confirmation for page tools.</violation>
<violation number="3" location="Resources/browser-repl/sites/webmcp.js:68">
P1: The WebMCP draft keeps the caller's mutable `input` object by reference, allowing the payload to change after the preview and before confirmation. Snapshot or validate the input when creating the draft, then use that snapshot for both the preview and confirmed execution.</violation>
</file>
<file name="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift">
<violation number="1" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:340">
P1: `tabs.close` accepts targets from `tabs.list({ all: true })`, but this call uses the session workspace for every target. Resolve `panel.workspaceId` before closing; otherwise a foreign tab remains open after the driver emits `tab.closed`.</violation>
<violation number="2" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:384">
P2: `tab.bringToFront` silently no-ops for tabs listed from another workspace because this lookup uses the session workspace. Resolve the workspace containing `panel.workspaceId` and select the panel there.</violation>
<violation number="3" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:531">
P2: This retry loop can return success after all 50 document evaluations fail. Track a successful evaluation and throw or continue until the timeout when none succeeds, so navigation cannot resolve before the requested load state.</violation>
<violation number="4" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:1081">
P2: A failed mouse-down permanently claims the tab's pointer for this session because release is deferred only for `up`. Clear the pointer claim on every failed down path so later sessions cannot hang waiting for a button that was never delivered.</violation>
</file>
<file name="Sources/TerminalController+BrowserRepl.swift">
<violation number="1" location="Sources/TerminalController+BrowserRepl.swift:116">
P1: Named session lookup ignores the resolved workspace, so reusing a session name from another workspace silently drives the original workspace's tabs. Scope the registry key by workspace ID or reject a workspace mismatch before evaluating.</violation>
</file>
<file name="Resources/browser-repl/sites/google.js">
<violation number="1" location="Resources/browser-repl/sites/google.js:58">
P1: Slides exports use the wrong Google endpoint shape here. Use `/presentation/d/${ref.id}/export/${format}` for `presentation` refs (preserving `authuser` as a query parameter), while retaining the query-form endpoint for Docs and Sheets.</violation>
</file>
<file name="Sources/Panels/BrowserRepl/BrowserReplCredentialRequest.swift">
<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplCredentialRequest.swift:59">
P1: The secret-bearing fill runs in the same content world exposed to arbitrary agent evaluations, so an agent can install a setter or event hook and capture the credentials. Execute the fill through an app-owned world or another mechanism that does not share mutable JS state with REPL code.</violation>
</file>
<file name="tests/browser-parity/compare/chatgpt-formats.mjs">
<violation number="1" location="tests/browser-parity/compare/chatgpt-formats.mjs:104">
P1: This key does not identify a stable frame or document: it combines a per-call frame position with a per-document local ref. When iframe visibility/order changes or the page navigates, an old `node_id` is reused for a different element, so subsequent ref-based automation can act on the wrong control; track stable frame/document generations and invalidate mappings when they change.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTimerScheduler.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTimerScheduler.swift:157">
P1: The scheduler commits every due timer before invoking any callback, so an earlier callback cannot cancel a later due timer. Keep due entries cancellable until each callback is dispatched, or dispatch one due timer at a time and re-check its registration after each callback.</violation>
</file>
<file name="CLI/cmux.swift">
<violation number="1" location="CLI/cmux.swift:18144">
P1: The `repl` branch silently ignores an explicitly supplied surface handle. `cmux browser --surface <surface> repl ...` can therefore run against the focused workspace instead of the requested browser context; reject surface-qualified REPL commands or translate the handle to its workspace before dispatch.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift:93">
P2: Automation drops never notify WebKit's original drag source when they end. Preserve the `source` passed to `beginDraggingSession` and invoke its `draggingSession(_:endedAt:operation:)` callback; otherwise the page's `dragend` lifecycle is skipped and drag state can remain active.</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift:233">
P1: Session teardown clears the bookkeeping without sending modifier-release events, leaving WebKit with stuck modifiers after a REPL disconnect or exception. Replay a release/`flagsChanged` event for each held key before removing it from the owner.</violation>
</file>
<file name="Sources/Panels/BrowserRepl/BrowserReplCapture.swift">
<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplCapture.swift:47">
P1: This edge-only limit still permits a 268-megapixel screenshot and can exhaust the app's memory. Apply a total pixel-area cap before taking the WebKit snapshot, not only per-dimension clamps.</violation>
<violation number="2" location="Sources/Panels/BrowserRepl/BrowserReplCapture.swift:225">
P2: The cookie path check ignores the required path-boundary rule, so `/account` cookies match `/accounting`. Match an exact path, a slash-terminated cookie path, or a prefix followed by `/`.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift:89">
P1: A long-running evaluation is closed at the idle deadline. `BrowserReplSession` records `lastUsed` when evaluation starts, so an evaluation longer than `idleTimeout` makes this condition false and closes its active JS thread, despite the comment saying long evaluations should be kept; track in-flight evaluations or refresh usage before expiring.</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift:93">
P1: This expiry path can close a newly created session belonging to the same name. The callback releases the registry lock before the name-based reset, so a concurrent reset/recreate can install a replacement before this call; revalidate the timer ID and session identity under the lock before removing and closing it.</violation>
</file>
<file name="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift">
<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift:283">
P1: Visibility changes must reevaluate the render host instead of always releasing it. Selecting the tab in a non-key window loses automation focus, while switching away stops hidden-tab rendering until the next command; call `keepRendering()` here so it chooses the live pane only when appropriate.</violation>
<violation number="2" location="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift:358">
P1: `detachAll` must release pointer waiters and clear `pointerOwner`; otherwise closing a tab while another session is waiting for its pointer serializes that session forever.</violation>
</file>
<file name="Resources/browser-repl/sites/notion.js">
<violation number="1" location="Resources/browser-repl/sites/notion.js:131">
P2: Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building `content` operations, or reject nested list input instead of silently changing its structure.</violation>
<violation number="2" location="Resources/browser-repl/sites/notion.js:143">
P1: This adapter lets `options.origin` redirect Notion API calls and confirmed page edits away from `https://www.notion.so`, so a mistaken or injected option can transmit page content to an arbitrary site. Reject non-Notion origins or always use the fixed Notion origin.</violation>
</file>
<file name="Resources/browser-repl/sites/google-calendar.js">
<violation number="1" location="Resources/browser-repl/sites/google-calendar.js:59">
P2: Date-only ISO inputs are parsed as UTC, but this route uses local getters. Normalize date-only values as calendar dates or use UTC components so `events({ date: "2026-09-30" })` selects the requested day.</violation>
<violation number="2" location="Resources/browser-repl/sites/google-calendar.js:100">
P1: The guest path can report success when the invitation dialog never exposes `Send`, leaving guests uninformed. Do not swallow this wait failure; require and click the invitation control, or explicitly verify an equivalent sent state.</violation>
</file>
<file name="Resources/browser-repl/api.js">
<violation number="1" location="Resources/browser-repl/api.js:424">
P1: Google exports use the current tab's cookies instead of the `page` being exported. Pass the exported page's target ID/cookie context into the fetch used by `createExporter`, otherwise `page.exportContent()` fails or can export from the wrong account for background tabs.</violation>
</file>
<file name="Sources/Panels/BrowserRepl/BrowserReplNativeInput.swift">
<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplNativeInput.swift:95">
P1: These conversions can crash cmux before `Int32(clamping:)` runs: a valid JSON wheel delta outside `Int` range traps during `Int(...)` conversion. Validate finite values and clamp the `Double` to the `Int32` range before converting so invalid agent input returns safely instead of terminating the app.</violation>
</file>
<file name="Resources/browser-repl/sites/loader.js">
<violation number="1" location="Resources/browser-repl/sites/loader.js:191">
P2: `parseA1Range` accepts invalid A1 ranges such as `A0`, `A1:B0`, and `B2:A1`; `googleSheets.read` then passes the resulting negative or reversed indexes to `slice`, silently returning incorrect or empty rows. Validate 1-based row numbers and range ordering before returning the parsed range.</violation>
<violation number="2" location="Resources/browser-repl/sites/loader.js:366">
P1: Drafts retain mutable caller objects, so Slack and Calendar can send changed values after the user approved the original preview. Snapshot the input before invoking the draft factory and close the send operation over that snapshot.</violation>
</file>
<file name="Resources/browser-repl/sites/page-assets.js">
<violation number="1" location="Resources/browser-repl/sites/page-assets.js:83">
P2: `list()` truncates inline SVG markup that `bundle()` later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.</violation>
<violation number="2" location="Resources/browser-repl/sites/page-assets.js:121">
P2: `bundle()` can overwrite a downloaded asset named `manifest.json` when it writes its manifest. Reserve the manifest filename before allocating asset filenames so the returned asset remains intact.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift:64">
P1: The canonical-path check is vulnerable to a symlink TOCTOU race: an attacker can replace a checked parent after `resolve` returns and redirect the subsequent filesystem operation outside the sandbox. Perform these operations through descriptor-relative handles with no-follow constraints, or revalidate while holding the relevant directory descriptors.
(Based on your team's feedback about descriptor-relative filesystem operations.)</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift:65">
P2: The read-only download exception is bypassed for every downloaded file under the temporary directory because the writable `additionalRoots` check runs first. Reject write access to any path in `readableFiles` before accepting `root` or `additionalRoots`.</violation>
</file>
<file name="CLI/CMUXCLI+BrowserRepl.swift">
<violation number="1" location="CLI/CMUXCLI+BrowserRepl.swift:114">
P2: Large code, especially newline-heavy code, can exceed the socket request buffer after JSON escaping and fail as a transport error. Call `ensureTextFitsSocketRequest` for every cell before sending it.</violation>
<violation number="2" location="CLI/CMUXCLI+BrowserRepl.swift:174">
P2: This reads the entire stdin stream before enforcing the 15 MiB limit, allowing a large or unbounded pipe to consume the CLI’s memory. Read bounded chunks and fail as soon as the limit is crossed.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift:160">
P2: `storeCookies` drops `Secure` `Set-Cookie` responses received over HTTP, including the loopback HTTP origins this browser supports. Parse Secure cookies against an HTTPS URL, as `BrowserCookieBuilder` does, while retaining the response URL for cookie scope.</violation>
<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift:237">
P1: `browserRepl(from:)` does not reliably preserve the `HttpOnly` flag when converting the Playwright shape back to `HTTPCookie`. Build HttpOnly cookies through the existing Set-Cookie reparse path so fetch-written authentication cookies remain hidden from page JavaScript.</violation>
</file>
<file name="Resources/browser-repl/sites/linkedin.js">
<violation number="1" location="Resources/browser-repl/sites/linkedin.js:125">
P1: The compose check validates only the first 40 characters, so a truncated or modified remainder can still be posted after confirmation. Compare the normalized full composer text with the drafted text before clicking Post.</violation>
</file>
<file name="Resources/browser-repl/sites/x.js">
<violation number="1" location="Resources/browser-repl/sites/x.js:85">
P2: `statusId` accepts arbitrary strings that merely end in digits, so malformed post IDs can silently target another post. Anchor the accepted bare-ID and X/Twitter status-URL forms before creating a tweet URL or reply draft.</violation>
<violation number="2" location="Resources/browser-repl/sites/x.js:143">
P2: The compose safety check validates only the first 40 characters, so X can publish a draft whose remaining text differs from the confirmed preview. Compare the complete whitespace-normalized composer text before clicking Post.</violation>
</file>
<file name="Resources/browser-repl/page-agent.js">
<violation number="1" location="Resources/browser-repl/page-agent.js:1066">
P2: File choosers opened by file inputs inside shadow roots are associated with the shadow host instead of the input because this document-level listener uses the retargeted `event.target`. Read the original target from `event.composedPath()[0]` before checking for `HTMLInputElement`, so shadow-root uploads resolve the correct handle.</violation>
</file>
<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplKeyStroke.swift">
<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplKeyStroke.swift:83">
P2: Control-modified digits and punctuation still carry their printable base character. The runtime sends no text for these keys, but this branch excludes Control, so native replay can insert `1` or `;` into editable fields; clear printable characters for Control before the later letter-to-control conversion.</violation>
</file>
<file name="Resources/browser-repl/sites/google-drive.js">
<violation number="1" location="Resources/browser-repl/sites/google-drive.js:59">
P2: `googleDrive.export` cannot export a Google document, sheet, or presentation supplied as a Drive URL when `{ kind }` is provided: `g.parse` leaves the URL as `kind: "file"`, so this branch always throws. Allow a supplied Google kind to classify Drive URLs (while retaining the uploaded-file error when no kind is supplied), or change the documented API.</violation>
</file>
<file name="Resources/browser-repl/sites/auth-fill.js">
<violation number="1" location="Resources/browser-repl/sites/auth-fill.js:9">
P2: Shadow-root credential fields cannot be filled: `document.querySelector` only searches the document tree, while the preceding locator flow accepts elements inside shadow roots. Resolve the marked element through the agent world's shadow-root traversal (including the closed-root support used by the browser API) rather than querying only the document.</violation>
</file>
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Re-trigger cubic
| if fileManager.fileExists(atPath: to) { | ||
| try fileManager.removeItem(atPath: to) | ||
| } | ||
| try fileManager.moveItem(atPath: from, toPath: to) |
There was a problem hiding this comment.
P0: rename and copyFile remove an existing destination before the source operation succeeds. A missing source or from == to can destroy data, and to == "." bypasses rm’s root guard; reject root endpoints and use source-first atomic replacement instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift, line 134:
<comment>`rename` and `copyFile` remove an existing destination before the source operation succeeds. A missing source or `from == to` can destroy data, and `to == "."` bypasses `rm`’s root guard; reject root endpoints and use source-first atomic replacement instead.</comment>
<file context>
@@ -0,0 +1,205 @@
+ if fileManager.fileExists(atPath: to) {
+ try fileManager.removeItem(atPath: to)
+ }
+ try fileManager.moveItem(atPath: from, toPath: to)
+ return NSNull()
+ case "copyFile":
</file context>
| }, | ||
| // Any read-only GraphQL query; mutations go through the Linear UI. | ||
| async query(text, variables) { | ||
| if (/^\s*mutation\b/i.test(String(text))) throw new S.SiteError("write_requires_draft", "linear.query: mutations are not run by site tools; make the change in the Linear page"); |
There was a problem hiding this comment.
P1: This guard only rejects documents whose first token is mutation; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/linear.js, line 64:
<comment>This guard only rejects documents whose first token is `mutation`; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.</comment>
<file context>
@@ -0,0 +1,71 @@
+ },
+ // Any read-only GraphQL query; mutations go through the Linear UI.
+ async query(text, variables) {
+ if (/^\s*mutation\b/i.test(String(text))) throw new S.SiteError("write_requires_draft", "linear.query: mutations are not run by site tools; make the change in the Linear page");
+ return q(String(text), variables);
+ },
</file context>
| } | ||
| guard let self else { return } | ||
| let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000 | ||
| self.thread.perform { |
There was a problem hiding this comment.
P1: This timeout cannot interrupt synchronous JavaScript because it queues finishEval on the thread currently executing evalFunction.call(...). A cell such as while (true) {} therefore hangs the evaluation and blocks close() as well; use an interruptible JavaScript execution/watchdog path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift, line 238:
<comment>This timeout cannot interrupt synchronous JavaScript because it queues `finishEval` on the thread currently executing `evalFunction.call(...)`. A cell such as `while (true) {}` therefore hangs the evaluation and blocks `close()` as well; use an interruptible JavaScript execution/watchdog path.</comment>
<file context>
@@ -0,0 +1,490 @@
+ }
+ guard let self else { return }
+ let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
+ self.thread.perform {
+ self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms")
+ }
</file context>
| guard let self else { return } | ||
| let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000 | ||
| self.thread.perform { | ||
| self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms") |
There was a problem hiding this comment.
P1: The timeout only completes the Swift continuation; it leaves the JavaScript __cmuxReplEval promise running. Because repl-host.js serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift, line 239:
<comment>The timeout only completes the Swift continuation; it leaves the JavaScript `__cmuxReplEval` promise running. Because `repl-host.js` serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.</comment>
<file context>
@@ -0,0 +1,490 @@
+ guard let self else { return }
+ let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
+ self.thread.perform {
+ self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms")
+ }
+ }
</file context>
| const shown = (await box.innerText()).replace(/\s+/g, " "); | ||
| if (bodyStart && !shown.includes(bodyStart.replace(/\s+/g, " "))) throw new S.SiteError("compose_mismatch", "gmail.send: the compose window did not receive the drafted body; nothing was sent"); | ||
| await page.locator('div[role="button"][data-tooltip^="Send"], div[role="button"][aria-label^="Send"]').last().click(); | ||
| await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" }); |
There was a problem hiding this comment.
P1: finishSend searches the whole document for Message sent, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/gmail.js, line 144:
<comment>`finishSend` searches the whole document for `Message sent`, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.</comment>
<file context>
@@ -0,0 +1,205 @@
+ const shown = (await box.innerText()).replace(/\s+/g, " ");
+ if (bodyStart && !shown.includes(bodyStart.replace(/\s+/g, " "))) throw new S.SiteError("compose_mismatch", "gmail.send: the compose window did not receive the drafted body; nothing was sent");
+ await page.locator('div[role="button"][data-tooltip^="Send"], div[role="button"][aria-label^="Send"]').last().click();
+ await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" });
+ // Gmail holds a sent message for its undo window in this page; keep
+ // the tab until the Undo action is gone.
</file context>
| await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" }); | |
| await t.waitIn(page, () => [...document.querySelectorAll('[role="alert"], .bAq')].some((e) => /Message sent/.test(e.innerText)), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" }); |
| const inlineSvgs = svgs.map((s, i) => { | ||
| const label = s.getAttribute("aria-label") || (s.querySelector("title") || {}).textContent || s.id || "svg-" + (i + 1); | ||
| const markup = s.outerHTML; | ||
| return { name: String(label).trim().slice(0, 60), markup: markup.length > arg.maxSvgChars ? markup.slice(0, arg.maxSvgChars) + "<!-- truncated -->" : markup }; |
There was a problem hiding this comment.
P2: list() truncates inline SVG markup that bundle() later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/page-assets.js, line 83:
<comment>`list()` truncates inline SVG markup that `bundle()` later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.</comment>
<file context>
@@ -0,0 +1,171 @@
+ const inlineSvgs = svgs.map((s, i) => {
+ const label = s.getAttribute("aria-label") || (s.querySelector("title") || {}).textContent || s.id || "svg-" + (i + 1);
+ const markup = s.outerHTML;
+ return { name: String(label).trim().slice(0, 60), markup: markup.length > arg.maxSvgChars ? markup.slice(0, arg.maxSvgChars) + "<!-- truncated -->" : markup };
+ });
+ return { pageUrl: location.href, assets: [...found.values()], inlineSvgs };
</file context>
| let bare = domain.hasPrefix(".") ? String(domain.dropFirst()) : domain | ||
| guard host == bare || host.hasSuffix("." + bare) else { return false } | ||
| let path = url.path.isEmpty ? "/" : url.path | ||
| guard path.hasPrefix(cookie.path) else { return false } |
There was a problem hiding this comment.
P2: The cookie path check ignores the required path-boundary rule, so /account cookies match /accounting. Match an exact path, a slash-terminated cookie path, or a prefix followed by /.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Sources/Panels/BrowserRepl/BrowserReplCapture.swift, line 225:
<comment>The cookie path check ignores the required path-boundary rule, so `/account` cookies match `/accounting`. Match an exact path, a slash-terminated cookie path, or a prefix followed by `/`.</comment>
<file context>
@@ -0,0 +1,251 @@
+ let bare = domain.hasPrefix(".") ? String(domain.dropFirst()) : domain
+ guard host == bare || host.hasSuffix("." + bare) else { return false }
+ let path = url.path.isEmpty ? "/" : url.path
+ guard path.hasPrefix(cookie.path) else { return false }
+ return !cookie.isSecure || url.scheme == "https" || isLoopback(host)
+ }
</file context>
| if (!loc || typeof loc.count !== "function") throw new S.SiteError("invalid", `browserAuth.request: field ${f.id}: selector: expected a selector string or a locator`); | ||
| if ((await loc.count()) !== 1) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_unique" } }; | ||
| if (!(await loc.isVisible())) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_user_visible" } }; | ||
| const tag = await loc.evaluate((el) => (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && !el.disabled && !el.readOnly); |
There was a problem hiding this comment.
P2: This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict HTMLInputElement to the supported text-like types (or require the DOM type to match f.type) before marking the element for credential fill.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/browser-auth.js, line 46:
<comment>This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict `HTMLInputElement` to the supported text-like types (or require the DOM type to match `f.type`) before marking the element for credential fill.</comment>
<file context>
@@ -0,0 +1,92 @@
+ if (!loc || typeof loc.count !== "function") throw new S.SiteError("invalid", `browserAuth.request: field ${f.id}: selector: expected a selector string or a locator`);
+ if ((await loc.count()) !== 1) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_unique" } };
+ if (!(await loc.isVisible())) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_user_visible" } };
+ const tag = await loc.evaluate((el) => (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && !el.disabled && !el.readOnly);
+ if (!tag) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_editable_text_field" } };
+ // The frame that holds the element (a frameLocator chain ends in a child frame).
</file context>
| // Calls a tool. readOnlyHint tools run now; others return a draft that | ||
| // call(draftId, { confirm: true }) runs. Options: { page }. | ||
| async call(name, input, options = {}) { | ||
| if (typeof name === "string" && /^draft-\d+-[0-9a-f]+$/.test(name)) return t.write("webmcp", "call", name, input); |
There was a problem hiding this comment.
P2: This makes a valid page tool named like draft-1-abcdef impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.
(Based on your team's feedback about user-representable string sentinels.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/webmcp.js, line 61:
<comment>This makes a valid page tool named like `draft-1-abcdef` impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.
(Based on your team's feedback about user-representable string sentinels.) </comment>
<file context>
@@ -0,0 +1,82 @@
+ // Calls a tool. readOnlyHint tools run now; others return a draft that
+ // call(draftId, { confirm: true }) runs. Options: { page }.
+ async call(name, input, options = {}) {
+ if (typeof name === "string" && /^draft-\d+-[0-9a-f]+$/.test(name)) return t.write("webmcp", "call", name, input);
+ const page = options.page || t.currentPage();
+ const { tools } = await list(page);
</file context>
| out.push({ type: "code", properties: { title: [[code.join("\n")]], language: [[m[1] || "Plain Text"]] } }); | ||
| } else if ((m = /^(#{1,3})\s+(.*)$/.exec(line))) out.push({ type: ["header", "sub_header", "sub_sub_header"][m[1].length - 1], properties: { title: richText(m[2]) } }); | ||
| else if ((m = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/.exec(line))) out.push({ type: "to_do", properties: { title: richText(m[2]), checked: [[m[1] === " " ? "No" : "Yes"]] } }); | ||
| else if ((m = /^\s*[-*]\s+(.*)$/.exec(line))) out.push({ type: "bulleted_list", properties: { title: richText(m[1]) } }); |
There was a problem hiding this comment.
P2: Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building content operations, or reject nested list input instead of silently changing its structure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 131:
<comment>Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building `content` operations, or reject nested list input instead of silently changing its structure.</comment>
<file context>
@@ -0,0 +1,246 @@
+ out.push({ type: "code", properties: { title: [[code.join("\n")]], language: [[m[1] || "Plain Text"]] } });
+ } else if ((m = /^(#{1,3})\s+(.*)$/.exec(line))) out.push({ type: ["header", "sub_header", "sub_sub_header"][m[1].length - 1], properties: { title: richText(m[2]) } });
+ else if ((m = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/.exec(line))) out.push({ type: "to_do", properties: { title: richText(m[2]), checked: [[m[1] === " " ? "No" : "Yes"]] } });
+ else if ((m = /^\s*[-*]\s+(.*)$/.exec(line))) out.push({ type: "bulleted_list", properties: { title: richText(m[1]) } });
+ else if ((m = /^\s*\d+[.)]\s+(.*)$/.exec(line))) out.push({ type: "numbered_list", properties: { title: richText(m[1]) } });
+ else if ((m = /^>\s?(.*)$/.exec(line))) out.push({ type: "quote", properties: { title: richText(m[1]) } });
</file context>
There was a problem hiding this comment.
7 issues found across 13 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Resources/browser-repl/sites/google-drive.js">
<violation number="1" location="Resources/browser-repl/sites/google-drive.js:31">
P2: The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full `<name> <type>` tooltip as `title`.</violation>
</file>
<file name="Resources/browser-repl/sites/linkedin.js">
<violation number="1" location="Resources/browser-repl/sites/linkedin.js:89">
P2: The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to `readCards()` so unkeyed or partially-rendered list items do not cause `feed()` to return an empty result.</violation>
</file>
<file name="Resources/browser-repl/sites/slack.js">
<violation number="1" location="Resources/browser-repl/sites/slack.js:56">
P2: Signed-out Slack operations now wait the full 30 seconds before reporting `not_signed_in`. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.</violation>
</file>
<file name="Resources/browser-repl/sites/notion.js">
<violation number="1" location="Resources/browser-repl/sites/notion.js:149">
P2: The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report `not_signed_in` even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update `apiOrigin` to the successful origin.</violation>
<violation number="2" location="Resources/browser-repl/sites/notion.js:150">
P2: The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.</violation>
</file>
<file name="Resources/browser-repl/sites/google.js">
<violation number="1" location="Resources/browser-repl/sites/google.js:102">
P2: The `\n{3,}` collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.</violation>
</file>
<file name="tests/browser-parity/sites/google-workspace.test.mjs">
<violation number="1" location="tests/browser-parity/sites/google-workspace.test.mjs:22">
P3: This deepEqual freezes `googleDocs.read` (md) output that keeps the image reference `![][image1]` while the strip in `google.js` `exportText` removes the `[image1]: <data:...>` definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of `data:image`, so it would pass either way.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const TYPES = ["Google Docs", "Google Sheets", "Google Slides", "Google Forms", "Google Drawings", "Google Sites", "Google Apps Script", "Shared folder", "Folder", "PDF", "Image", "Video", "Audio", "Microsoft Word", "Microsoft Excel", "Microsoft PowerPoint", "Text", "Archive", "Unknown"]; | ||
| const tip = clean((el.querySelector("[data-tooltip]") || {}).getAttribute ? el.querySelector("[data-tooltip]").getAttribute("data-tooltip") : ""); | ||
| const type = TYPES.find((x) => tip === x || tip.endsWith(" " + x)) || null; | ||
| let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip; |
There was a problem hiding this comment.
P2: The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full <name> <type> tooltip as title.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 31:
<comment>The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full `<name> <type>` tooltip as `title`.</comment>
<file context>
@@ -9,6 +9,36 @@
+ const TYPES = ["Google Docs", "Google Sheets", "Google Slides", "Google Forms", "Google Drawings", "Google Sites", "Google Apps Script", "Shared folder", "Folder", "PDF", "Image", "Video", "Audio", "Microsoft Word", "Microsoft Excel", "Microsoft PowerPoint", "Text", "Archive", "Unknown"];
+ const tip = clean((el.querySelector("[data-tooltip]") || {}).getAttribute ? el.querySelector("[data-tooltip]").getAttribute("data-tooltip") : "");
+ const type = TYPES.find((x) => tip === x || tip.endsWith(" " + x)) || null;
+ let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip;
+ if (!title) title = clean((el.innerText || "").split("\n")[0]);
+ out.push({ id, title, type, url: "https://drive.google.com/open?id=" + id });
</file context>
| let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip; | |
| let title = type ? clean(tip.slice(0, tip.length - type.length)) : clean((el.querySelector(".name, [data-column-field='6'], strong") || {}).textContent || (el.innerText || "").split("\n")[0]); |
| async function cards(url, kind, limit) { | ||
| return t.withTab(url, async (page) => { | ||
| t.assertSignedIn("linkedin", page, SIGN_IN); | ||
| await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 }); |
There was a problem hiding this comment.
P2: The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to readCards() so unkeyed or partially-rendered list items do not cause feed() to return an empty result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/linkedin.js, line 89:
<comment>The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to `readCards()` so unkeyed or partially-rendered list items do not cause `feed()` to return an empty result.</comment>
<file context>
@@ -72,7 +86,7 @@
return t.withTab(url, async (page) => {
t.assertSignedIn("linkedin", page, SIGN_IN);
- await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });
+ await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });
let got = await page.evaluate(readCards, { kind, limit });
for (let i = 0; i < 6 && got.length < limit; i++) {
</file context>
| await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 }); | |
| await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"][componentkey] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 }); |
| t.withTab(APP + "/robots.txt", async (page) => { | ||
| if (!(await page.evaluate(hasConfig))) { | ||
| await page.goto(APP + "/client", { waitUntil: "load", timeout: 45000 }); | ||
| await t.waitIn(page, hasConfig, undefined, { timeout: 30000, what: "Slack's web client to load the workspaces", name: "slack" }).catch(() => {}); |
There was a problem hiding this comment.
P2: Signed-out Slack operations now wait the full 30 seconds before reporting not_signed_in. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/slack.js, line 56:
<comment>Signed-out Slack operations now wait the full 30 seconds before reporting `not_signed_in`. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.</comment>
<file context>
@@ -38,8 +39,26 @@
+ t.withTab(APP + "/robots.txt", async (page) => {
+ if (!(await page.evaluate(hasConfig))) {
+ await page.goto(APP + "/client", { waitUntil: "load", timeout: 45000 });
+ await t.waitIn(page, hasConfig, undefined, { timeout: 30000, what: "Slack's web client to load the workspaces", name: "slack" }).catch(() => {});
+ }
+ return body((fn, arg) => page.evaluate(fn, arg));
</file context>
| for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) { | ||
| rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId }); | ||
| if (rs[0] && rs[0].status !== 401) { | ||
| if (!options.origin) apiOrigin = o; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
P2: The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report not_signed_in even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update apiOrigin to the successful origin.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 149:
<comment>The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report `not_signed_in` even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update `apiOrigin` to the successful origin.</comment>
<file context>
@@ -141,8 +141,18 @@
async function calls(list, options = {}) {
- const rs = await t.inOrigin(origin(options), notionCall, { calls: list, userId: options.userId });
+ let rs;
+ for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
+ rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
+ if (rs[0] && rs[0].status !== 401) {
</file context>
| for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) { | |
| rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId }); | |
| if (rs[0] && rs[0].status !== 401) { | |
| if (!options.origin) apiOrigin = o; | |
| break; | |
| } | |
| } | |
| const origins = options.origin ? [options.origin] : apiOrigin ? [apiOrigin, ...API_ORIGINS.filter((o) => o !== apiOrigin)] : API_ORIGINS; | |
| for (const o of origins) { | |
| rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId }); | |
| if (rs[0] && rs[0].status !== 401) { | |
| if (!options.origin) apiOrigin = o; | |
| break; | |
| } | |
| } |
| async function calls(list, options = {}) { | ||
| let rs; | ||
| for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) { | ||
| rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId }); |
There was a problem hiding this comment.
P2: The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 150:
<comment>The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.</comment>
<file context>
@@ -141,8 +141,18 @@
- const rs = await t.inOrigin(origin(options), notionCall, { calls: list, userId: options.userId });
+ let rs;
+ for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
+ rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
+ if (rs[0] && rs[0].status !== 401) {
+ if (!options.origin) apiOrigin = o;
</file context>
| let text = await response.text(); | ||
| // Google's Markdown export embeds each image as a data: reference | ||
| // definition (most of a document with images); keep the references. | ||
| if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n"); |
There was a problem hiding this comment.
P2: The \n{3,} collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google.js, line 102:
<comment>The `\n{3,}` collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.</comment>
<file context>
@@ -96,7 +96,11 @@
+ let text = await response.text();
+ // Google's Markdown export embeds each image as a data: reference
+ // definition (most of a document with images); keep the references.
+ if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
+ return { title, text };
}
</file context>
| if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n"); | |
| if (format === "md") { | |
| const stripped = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, ""); | |
| if (stripped !== text) text = stripped.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n"); | |
| } |
|
|
||
| test("googleDocs.read: Markdown by default, title from the export's file name", async () => { | ||
| const r = await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit")'); | ||
| assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n\n![][image1]\n" }); |
There was a problem hiding this comment.
P3: This deepEqual freezes googleDocs.read (md) output that keeps the image reference ![][image1] while the strip in google.js exportText removes the [image1]: <data:...> definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of data:image, so it would pass either way.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/sites/google-workspace.test.mjs, line 22:
<comment>This deepEqual freezes `googleDocs.read` (md) output that keeps the image reference `![][image1]` while the strip in `google.js` `exportText` removes the `[image1]: <data:...>` definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of `data:image`, so it would pass either way.</comment>
<file context>
@@ -19,10 +19,16 @@ test("googleAccounts.list: signed-in accounts with their uid, from ListAccounts"
test("googleDocs.read: Markdown by default, title from the export's file name", async () => {
const r = await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit")');
- assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n" });
+ assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n\n![][image1]\n" });
assert.equal((await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit", { format: "txt" })')).text, "Design Notes\n\nThe plan, in brief.\n");
});
</file context>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
29 issues found across 27 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Resources/browser-repl/sites/google-sheets.js">
<violation number="1" location="Resources/browser-repl/sites/google-sheets.js:29">
P2: Empty rows pass validation and are reported as successfully written with an invalid range. Reject rows with zero cells before calculating `width`.</violation>
<violation number="2" location="Resources/browser-repl/sites/google-sheets.js:35">
P2: `write` accepts row zero even though it is not valid A1 notation. Reject non-positive row numbers before selecting the range.</violation>
<violation number="3" location="Resources/browser-repl/sites/google-sheets.js:120">
P3: An unknown `options.gid` silently falls back to the first tab (`book[0]`) and returns cells, while an unknown `options.sheet` throws `not_found`. An agent that asks `cells(sheet, { gid: "7" })` on a file without that tab gets the first sheet's data with no error. Throw `not_found` here, matching the `options.sheet` branch.</violation>
<violation number="4" location="Resources/browser-repl/sites/google-sheets.js:153">
P2: On a multi-tab sheet `append` measures the last non-empty row on the tab selected by `options.sheet`/`options.gid`, but the paste itself lands in the URL-gid tab: `selectRange` only sets the name box within the active sheet and never switches tabs. The verification then re-reads the `options`-selected tab via `api.cells`, so the tool writes one tab and verifies another (reporting `verified: false`, or worse confirming a coincidental match). The same mismatch exists for `write`/`clear` when `options.sheet` differs from the URL's `#gid`.</violation>
</file>
<file name="tests/browser-parity/scenarios/33-markdown-corpus.js">
<violation number="1" location="tests/browser-parity/scenarios/33-markdown-corpus.js:30">
P2: The `squash(want.name) !== own` gate silently skips every link whose rendered text differs from Chrome's accessible name — links with aria-label, icon links (innerText empty), and alt-named links — so those links are never checked against `md` and a regression that drops their label leaves `missing-links` empty. The header claims "every link Chrome lists that this engine shows keeps its text," but the check only covers links whose name equals innerText. Drop the name-equality gate and verify the rendered text of every link that has some; for textless icon links additionally check `flat.includes(squash(want.name))`.</violation>
<violation number="2" location="tests/browser-parity/scenarios/33-markdown-corpus.js:36">
P3: `noMarkup` only rejects `div`, `span`, `script`, and the `\u0000` frame marker, while the header promises "no HTML or frame placeholder is left outside code". A leak of any other tag — `<a href=…>`, `<main>`, `<nav>`, `<article>`, `<img>`, `<b>/<i>`, or raw `<iframe>` text — passes the golden check and would not be caught by this scenario. Broaden the tag set (e.g., `a|iframe|main|nav|article|b|i|em|strong|h1|h2|p|ul|ol`) or the leak test to a generic `/</?[a-z][\w-]*\b/` on the code-stripped text, or narrow the header claim to what the assertion actually enforces.</violation>
</file>
<file name="tests/browser-parity/unit/agent-tools.test.mjs">
<violation number="1" location="tests/browser-parity/unit/agent-tools.test.mjs:111">
P3: `withRepl` creates `dir` and `sessionTmp` outside the `try`, so if `createDevBrowser()`, `createNodeHost()`, or `createBrowserRepl()` throws (e.g., WebKit missing on the machine), the `finally` never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating `dir`/`sessionTmp` inside the `try` (with the cleanup in `finally`) or by guarding the setup with its own try/finally.</violation>
<violation number="2" location="tests/browser-parity/unit/agent-tools.test.mjs:361">
P3: This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a `sites/*.test.mjs` proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the `sites` directory and assert the named test files exist.</violation>
</file>
<file name="Resources/browser-repl/sites/google-editors.js">
<violation number="1" location="Resources/browser-repl/sites/google-editors.js:96">
P2: `parseDeck` only extracts direct text shapes, so grouped-shape and table text disappears from slide reads and replacement verification. Traverse all text-bearing slide descendants, including `p:grpSp` and table `p:graphicFrame` elements.</violation>
<violation number="2" location="Resources/browser-repl/sites/google-editors.js:122">
P2: `exportParts` reports an unauthenticated redirect as an unexpected non-ZIP export. Preserve the final response URL or detect the sign-in page and raise `not_signed_in`, so callers receive the documented recovery guidance.</violation>
</file>
<file name="Resources/browser-repl/runtime-core.js">
<violation number="1">
P2: This hook feeds redacted driver data back into runtime state, not only into user-visible output. When a registered secret appears in a URL or redirect, `page.url()` becomes the masked, nonexistent URL, so later URL-dependent operations target the wrong address; preserve raw payloads for internal bookkeeping and redact only copies crossing the REPL/output boundary.</violation>
</file>
<file name="Resources/browser-repl/repl-host.js">
<violation number="1" location="Resources/browser-repl/repl-host.js:310">
P2: The catch leaves a frozen or non-writable `Error` with its original message, so callers formatting returned `r.exception` can print a registered secret despite `r.error` being masked. Replace the exception with a redacted `Error` when mutation fails, or otherwise guarantee that the returned exception is safe to format.</violation>
</file>
<file name="Resources/browser-repl/sites/google-slides.js">
<violation number="1" location="Resources/browser-repl/sites/google-slides.js:28">
P2: `replace` counts speaker-note matches but the Find and replace action does not edit speaker notes, so it cannot fulfill its “every occurrence in the deck” contract. Either support note editing or exclude notes from the count and document the scope.</violation>
<violation number="2" location="Resources/browser-repl/sites/google-slides.js:35">
P2: This branch marks the edit verified without checking the deck whenever the replacement contains the search text. Verify the transformed slide content instead, so failed or partial Replace all operations cannot be reported as successful.</violation>
</file>
<file name="Resources/browser-repl/sites/google-drive.js">
<violation number="1" location="Resources/browser-repl/sites/google-drive.js:14">
P1: `trashNow` drops the account selector from parsed file URLs. Append `?authuser=${ref.uid}` when `ref.uid` is present, otherwise confirmed trash requests for non-default accounts open account 0.</violation>
<violation number="2" location="Resources/browser-repl/sites/google-drive.js:94">
P1: `create` loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with `{ uid: 1 }` without falling back to account 0.</violation>
</file>
<file name="Resources/browser-repl/sites/google-docs.js">
<violation number="1" location="Resources/browser-repl/sites/google-docs.js:18">
P2: `count` is case-sensitive even though these operations use Google Docs' case-insensitive Find and Replace. Mixed-case matches are reported with the wrong count, and `insertAfter` can pass its uniqueness check before replacing multiple matches; count matches using the same case-insensitive semantics as the editor.</violation>
</file>
<file name="Resources/browser-repl/api.js">
<violation number="1" location="Resources/browser-repl/api.js:569">
P1: The fetch domain policy checks only the initial URL, so redirects bypass `allowedDomains`, `prohibitedDomains`, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.</violation>
<violation number="2" location="Resources/browser-repl/api.js:592">
P2: Redacting serialized JSON before parsing can corrupt JSON when a registered secret contains JSON punctuation. Parse the response first and recursively redact returned values without replacing structural characters.</violation>
</file>
<file name="docs/browser-repl/site-tools.md">
<violation number="1">
P3: The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.</violation>
<violation number="2">
P2: This new section documents implemented editing tools, but the inventory table and Decision 6 elsewhere in the same file still say the opposite: Google Docs "Edits: not implemented (decision 6)", Google Sheets "Writes: decision 6", and Decision 6 describes the now-implemented feature in future tense ("A cmux version would be a draft of the diff, confirmed, applied with real input in the document tab"). Update those rows and remove or strike Decision 6 so the doc does not tell readers the tools do not exist.</violation>
</file>
<file name="Resources/browser-repl/agent-tools.js">
<violation number="1" location="Resources/browser-repl/agent-tools.js:94">
P1: `blockIPAddresses(true)` can be bypassed with integer, hexadecimal, or octal IPv4 spellings because `isIPHost` does not recognize them. Canonicalize the parsed host with a complete IPv4/IPv6 parser before applying the block.</violation>
<violation number="2" location="Resources/browser-repl/agent-tools.js:288">
P3: This branch is unreachable: `"DIALOG"` is in the `SKIP` set, and the preceding `if (SKIP.has(tag(el))) return true;` returns before `el.open` is ever consulted. Open `<dialog>` content (modals) is therefore always omitted from `page.markdown()` even though the code clearly intends to include it (closed dialogs are already hidden by their default `display: none`). Swap the two checks so an open dialog is exempted from `SKIP`.</violation>
<violation number="3" location="Resources/browser-repl/agent-tools.js:931">
P1: `session.record()` writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and `run.png`. Mask or omit secret-bearing frames before writing recording files.</violation>
<violation number="4" location="Resources/browser-repl/agent-tools.js:1044">
P2: `session.storageState()` reads cookies and localStorage through `session.call`, which the new `afterCall` hook runs through `redactValue`. When a registered secret value appears inside a stored cookie or localStorage item, the export writes `<secret:name>` in place of the real value, and `session.setStorageState()` (`cookies.set` / `localStorage.setItem`) then stores that literal masked string back — silently corrupting the auth state the round-trip is supposed to preserve. The same redaction feeds `api.js` `fetchWithCookies`: its dev-driver path (`host.fetchHandlesCookies` unset) builds the Cookie header from the redacted `cookies.get` result, so requests carry `<secret:name>` instead of the cookie. Masking belongs on output (prints, reads, files), not on data that is later reused as input; exclude `cookies.get`/localStorage reads used for storage state from masking, or keep a masked↔original mapping so `setStorageState` restores the true values.</violation>
<violation number="5" location="Resources/browser-repl/agent-tools.js:1057">
P1: `storageState({ path })` writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.</violation>
<violation number="6" location="Resources/browser-repl/agent-tools.js:1364">
P2: `page.scroll` relies on fixed sleeps and polling, so it can return before smooth scrolling settles and adds an unnecessary one-second cap. Await a scroll-settled signal or use a cancellation-aware wait tied to the actual scroll state.</violation>
</file>
<file name="tests/browser-parity/sites/live-diff.mjs">
<violation number="1" location="tests/browser-parity/sites/live-diff.mjs:250">
P3: The new `logged in` and `cookie found` substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.</violation>
</file>
<file name="tests/browser-parity/sites/mock-editors.mjs">
<violation number="1" location="tests/browser-parity/sites/mock-editors.mjs:161">
P2: Docs Find and replace skips table cells. Include each `b.rows` cell in the replacement and count so replacements in tables are reflected in subsequent exports.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| const g = S.shared.google; | ||
| const created = new Set(); | ||
| async function trashNow(ref) { | ||
| return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => { |
There was a problem hiding this comment.
P1: trashNow drops the account selector from parsed file URLs. Append ?authuser=${ref.uid} when ref.uid is present, otherwise confirmed trash requests for non-default accounts open account 0.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 14:
<comment>`trashNow` drops the account selector from parsed file URLs. Append `?authuser=${ref.uid}` when `ref.uid` is present, otherwise confirmed trash requests for non-default accounts open account 0.</comment>
<file context>
@@ -9,6 +9,17 @@
const g = S.shared.google;
+ const created = new Set();
+ async function trashNow(ref) {
+ return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => {
+ await t.waitIn(page, () => !!document.querySelector("#docs-file-menu"), undefined, { signIn: [/^https:\/\/accounts\.google\.com\//], name: "googleDrive.trash", what: "the editor's File menu", timeout: 45000 });
+ await page.locator("#docs-file-menu").click();
</file context>
| return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => { | |
| return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit${ref.uid !== undefined ? `?authuser=${ref.uid}` : ""}`, async (page) => { |
| await input.press("Enter"); | ||
| await t.sleep(800); | ||
| created.add(id); | ||
| return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title }; |
There was a problem hiding this comment.
P1: create loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with { uid: 1 } without falling back to account 0.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 94:
<comment>`create` loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with `{ uid: 1 }` without falling back to account 0.</comment>
<file context>
@@ -65,6 +76,38 @@
+ await input.press("Enter");
+ await t.sleep(800);
+ created.add(id);
+ return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title };
+ });
+ },
</file context>
| return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title }; | |
| return { id, url: `https://docs.google.com/${kind}/d/${id}/edit${q}`, title }; |
| const page = state.current && !state.current.isClosed() ? state.current : null; | ||
| const base = page && /^https?:/.test(page.url()) ? page.url() : undefined; | ||
| const url = new core.URL(String(input && input.url ? input.url : input), base).href; | ||
| if (session.agentTools) session.agentTools.checkURL("fetch", url); |
There was a problem hiding this comment.
P1: The fetch domain policy checks only the initial URL, so redirects bypass allowedDomains, prohibitedDomains, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/api.js, line 569:
<comment>The fetch domain policy checks only the initial URL, so redirects bypass `allowedDomains`, `prohibitedDomains`, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.</comment>
<file context>
@@ -560,11 +560,13 @@
const page = state.current && !state.current.isClosed() ? state.current : null;
const base = page && /^https?:/.test(page.url()) ? page.url() : undefined;
const url = new core.URL(String(input && input.url ? input.url : input), base).href;
+ if (session.agentTools) session.agentTools.checkURL("fetch", url);
const headers = {};
const src = init.headers || {};
</file context>
| } | ||
| } | ||
| const state = { cookies, origins: [...origins].map(([origin, localStorage]) => ({ origin, localStorage })) }; | ||
| if (options.path) fs.writeFileSync(options.path, JSON.stringify(state, null, 2)); |
There was a problem hiding this comment.
P1: storageState({ path }) writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 1057:
<comment>`storageState({ path })` writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.</comment>
<file context>
@@ -0,0 +1,1433 @@
+ }
+ }
+ const state = { cookies, origins: [...origins].map(([origin, localStorage]) => ({ origin, localStorage })) };
+ if (options.path) fs.writeFileSync(options.path, JSON.stringify(state, null, 2));
+ return state;
+ }
</file context>
| try { | ||
| const r = await session.call("tab.screenshot", { targetId: page._targetId, format: "png" }); | ||
| const file = path.join(recorder.dir, `frame-${String(++recorder.frames).padStart(4, "0")}.png`); | ||
| fs.writeFileSync(file, Buffer.from(r.base64, "base64")); |
There was a problem hiding this comment.
P1: session.record() writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and run.png. Mask or omit secret-bearing frames before writing recording files.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 931:
<comment>`session.record()` writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and `run.png`. Mask or omit secret-bearing frames before writing recording files.</comment>
<file context>
@@ -0,0 +1,1433 @@
+ try {
+ const r = await session.call("tab.screenshot", { targetId: page._targetId, format: "png" });
+ const file = path.join(recorder.dir, `frame-${String(++recorder.frames).padStart(4, "0")}.png`);
+ fs.writeFileSync(file, Buffer.from(r.base64, "base64"));
+ recorder.frameFiles.push(file);
+ return file;
</file context>
| for (const [, t] of unit[2].matchAll(/`([^`]+)`/g)) { | ||
| assert.equal(all.filter((x) => x.startsWith(t.replace(/…$/, ""))).length, 1, `unit/${unit[1]}: "${t}" names no single test`); | ||
| } | ||
| } else assert.ok(/^sites\/\*\.test\.mjs$/.test(part), `unknown proof "${part}" in: ${row}`); |
There was a problem hiding this comment.
P3: This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a sites/*.test.mjs proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the sites directory and assert the named test files exist.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/unit/agent-tools.test.mjs, line 361:
<comment>This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a `sites/*.test.mjs` proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the `sites` directory and assert the named test files exist.</comment>
<file context>
@@ -0,0 +1,364 @@
+ for (const [, t] of unit[2].matchAll(/`([^`]+)`/g)) {
+ assert.equal(all.filter((x) => x.startsWith(t.replace(/…$/, ""))).length, 1, `unit/${unit[1]}: "${t}" names no single test`);
+ }
+ } else assert.ok(/^sites\/\*\.test\.mjs$/.test(part), `unknown proof "${part}" in: ${row}`);
+ }
+ }
</file context>
|
|
||
| // One REPL session on the dev driver, with the app's output cap. | ||
| async function withRepl(fn, { maxOutput } = {}) { | ||
| const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cmux-repl-bu-"))); |
There was a problem hiding this comment.
P3: withRepl creates dir and sessionTmp outside the try, so if createDevBrowser(), createNodeHost(), or createBrowserRepl() throws (e.g., WebKit missing on the machine), the finally never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating dir/sessionTmp inside the try (with the cleanup in finally) or by guarding the setup with its own try/finally.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/unit/agent-tools.test.mjs, line 111:
<comment>`withRepl` creates `dir` and `sessionTmp` outside the `try`, so if `createDevBrowser()`, `createNodeHost()`, or `createBrowserRepl()` throws (e.g., WebKit missing on the machine), the `finally` never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating `dir`/`sessionTmp` inside the `try` (with the cleanup in `finally`) or by guarding the setup with its own try/finally.</comment>
<file context>
@@ -0,0 +1,364 @@
+
+// One REPL session on the dev driver, with the app's output cap.
+async function withRepl(fn, { maxOutput } = {}) {
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cmux-repl-bu-")));
+ const sessionId = `bu-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
+ const browser = await createDevBrowser();
</file context>
| @@ -0,0 +1,333 @@ | |||
| # Site tools (`sites`) | |||
There was a problem hiding this comment.
P3: The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/browser-repl/site-tools.md, line 165:
<comment>The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.</comment>
<file context>
@@ -131,6 +131,45 @@ error is a `SiteError` with a `code`: `invalid`, `not_signed_in`,
+| `googleDrive.trash(url)` | the editor's File > Move to trash | delete |
+
+Rule for writes (ChatGPT's confirmation taxonomy, [9] edits others can see):
+a write first opens the file's editor and reads its Share button. If it
+says "Private to only me", nobody else sees the edit and it runs at once.
+Otherwise, including when the sharing cannot be read, the write returns a
</file context>
|
|
||
| // Verdict from two summaries. | ||
| function verdict(op, c, a, cap) { | ||
| const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || ""); |
There was a problem hiding this comment.
P3: The new logged in and cookie found substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/sites/live-diff.mjs, line 250:
<comment>The new `logged in` and `cookie found` substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.</comment>
<file context>
@@ -247,7 +247,7 @@ async function signedIn() {
// Verdict from two summaries.
function verdict(op, c, a, cap) {
- const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login/i.test(r.error || "");
+ const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || "");
if (!c.ok && unavailable(c)) return { verdict: "cmux-unavailable", note: "user not signed in to cmux" };
if (!a.ok && a.code === "no_tool") return c.ok ? { verdict: "cmux-better", note: "Aside has no tool for this read" } : { verdict: "cmux-worse", note: `cmux failed: ${c.code || ""} ${redact(c.error)}` };
</file context>
| const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || ""); | |
| const unavailable = (r) => r.code === "not_signed_in" || /not signed in|not logged in|logged out|sign in|login|invalid_auth|not_authed|unauthorized|cookies missing|no cookie found|session (may be )?expired/i.test(r.error || ""); |
| if (tag(el) === "DIALOG" && el.open) return false; | ||
| const cs = style(el); |
There was a problem hiding this comment.
P3: This branch is unreachable: "DIALOG" is in the SKIP set, and the preceding if (SKIP.has(tag(el))) return true; returns before el.open is ever consulted. Open <dialog> content (modals) is therefore always omitted from page.markdown() even though the code clearly intends to include it (closed dialogs are already hidden by their default display: none). Swap the two checks so an open dialog is exempted from SKIP.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 288:
<comment>This branch is unreachable: `"DIALOG"` is in the `SKIP` set, and the preceding `if (SKIP.has(tag(el))) return true;` returns before `el.open` is ever consulted. Open `<dialog>` content (modals) is therefore always omitted from `page.markdown()` even though the code clearly intends to include it (closed dialogs are already hidden by their default `display: none`). Swap the two checks so an open dialog is exempted from `SKIP`.</comment>
<file context>
@@ -0,0 +1,1433 @@
+ };
+ const skipped = (el, ctx) => {
+ if (SKIP.has(tag(el))) return true;
+ if (tag(el) === "DIALOG" && el.open) return false;
+ const cs = style(el);
+ if (cs.display === "none" || cs.contentVisibility === "hidden") return true;
</file context>
| if (tag(el) === "DIALOG" && el.open) return false; | |
| const cs = style(el); | |
| if (tag(el) === "DIALOG" && el.open) return false; | |
| if (SKIP.has(tag(el))) return true; |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
A grid cell that drops text arriving without a keydown or a composition (Google Sheets' editor in WebKit) and reads pastes from clipboardData. Red: the app inserts text without a composition and pastes by inserting text, so no paste event fires. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tools eval, snapshot, screenshot, tabs and reset run in one REPL session (--session, mcp by default) over browser.repl.eval and browser.repl.reset. The JSON-RPC handling is a socket-free type; a node test drives the built CLI against a fake control socket when PARITY_CMUX_CLI is set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A default save carried every cookie in the user's profile. It now covers
the registrable domain of the current tab (page.context().storageState()
of that page); { all: true } keeps the whole-profile save and { urls }
is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d paste Google Sheets' cell editor in WebKit starts an edit only on a keydown or a composition: insertText reached the DOM but not the sheet, and Meta+V inserted text with no paste event. insertText now sets marked text and confirms it (compositionstart, beforeinput/input, compositionend, all trusted). Copy, Cut and Paste run WebKit's own editing commands with the general pasteboard lookup redirected to a private pasteboard holding the tab's clipboard, so pages get trusted clipboard events with clipboardData and the system clipboard stays untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
session.configure({ userAgent, extraHTTPHeaders, permissions, proxy })
applies Playwright browser-context options to the tabs a session drives:
the user agent overrides the destination policy, headers go on main-frame
GET navigations through the user-agent restart path, camera, microphone,
geolocation and notification requests answer from the granted list, and a
proxy puts tabs opened afterwards in a private data store routed through
it. The domain policy also compiles to a WebKit content rule list, so
images, scripts, XHR, WebSockets and iframes from other domains are
blocked too. browser-use-parity proofs may name diff cases.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each capture first sets -webkit-text-security: disc on text fields whose value holds a secret and on elements whose own text holds one, in every frame and shadow root, and restores them after. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…te is dropped The mock Sheets editor now reads a paste from clipboardData, as Sheets does in WebKit, and records whether each edit was pasted or typed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Meta+V now a trusted paste carrying the tab's clipboard, one paste of the rows as TSV writes the whole range. When the export does not show the values, the cells are typed as before. The dev driver dispatches a paste event with clipboardData, as the app's paste does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…refixed requests A navigation the driver starts carries session.configure's headers from the first request, so no policy restart races the navigation's wait. WebKit sanitizes pasted HTML, so the trusted-paste case checks the bold text rather than the markup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…one input event A composition into a text field fired three input events where Playwright's fill fires one (04-actions-diff trusted-log on the app). Fields get a plain insert again; contenteditable editors such as Sheets' cell editor get the composition. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All 158 cases recorded on the tagged app at 0a2c79e, including the new Sheets-style editor, trusted paste and session.configure cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves the driver's ObjectIdentifier-based data store id into the package unchanged (WKWebsiteDataStore.browserReplID) so the test can reach it.
dataStoreID came from the store's ObjectIdentifier, whose address a later
store takes once the first is freed, so tabs.open({ dataStore }) with an
old id could open a tab in another store. Each store now carries a UUID as
an associated object.
insertText recorded the secret for other sessions' masks before the check, so a value refused for its domain still became a mask other sessions saw. The record now happens in checkTarget after the check, on the same main-actor turn as the commit, so it still precedes typing.
- A window a user's tab opens while it handles the session's own input becomes a background tab for that session (tab.created, userOwned) under its domain policy, instead of a key popup window over the user's work; it stays the user's and is never closed with the session. - frame.evaluate counts as the session's action (el.click() that opens confirm() no longer shows cmux's dialog), as does a wake or a crash recovery; a navigation's input window ends when it commits, so a dialog from the user's own click while the page loads stays the user's. - The wake's commit wait ends on an invalidated web view instead of spinning on the main actor, and a cancelled call stops waiting. - cmux browser press runs Command+A/C/X/V/Z on the web view itself, since the resend of its keys to the Edit menu is dropped. - tab.handleEvents, tabs.activate and tab.bringToFront no longer wake a tab; the domain policy judges a hibernated tab's recorded URL before a wake loads it; tab.reload of a crashed or hibernated tab keeps its timeout and waitUntil. - The dev driver mirrors frame.evaluate and the commit-bounded navigation window.
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Since dcd478a the runtime names the page's tab on every cookie call, so the guard on tabs showing a blocked page also refused cookies.get and cookies.set: with allowedDomains(www.parent.test) the agent could not set a cookie for the allowed host from a page that showed another site (38-cookie-guards set-allowed-host, red on cmux-dev and the app). get and set use the tab only to pick its data store and are judged by URL and cookie domain; cookies.clear takes its scope from the tab's site and stays guarded.
…ry directory Red: the dev host has no private temporary directory, and the runtime nests cmux-browser-repl/<session> inside the session's own (18-print on the app). The test only ever removes a directory under <tmp>/cmux-browser-repl.
…sts remove only their own directories The session's os.tmpdir() is already its own private directory (<tmp>/cmux-browser-repl/<session>-<random>-tmp, mode 0700), but images, exports, recordings and spills added cmux-browser-repl/<session> inside it again (18-print on the app). They now go straight into it; the native session still removes it at close only when empty, so the files stay. The dev host gets the same private directory, so cmux-dev runs as the app does. Test cleanup: every recursive removal in tests/browser-parity goes through lib/test-dirs.mjs, which removes only a directory this process made with mkdtemp under the system temporary directory and refuses the temporary directory itself, /tmp, the home directory, the working directory and their parents. (An earlier version of the new test removed the shared temporary directory.) The cmux backend runs each scenario in a new working directory instead of the checkout, and 21-fs no longer deletes files it did not make.
Red: every frame.evaluate, the runtime's own agent-world reads and polls included, held the session's action window for the whole call, so during a long evaluation the user's own dialogs and popups in that tab went to the agent.
…ater popups in the background - Only page-world frame.evaluate (the agent's own script) opens the session's action window, and for at most a second on the injected clock; the runtime's agent-world reads and polls hold none, so during a long evaluation the user's own dialogs and popups stay the user's. - A window the page of a driven tab opens through the browser's path while the user is not working in that tab (not shown and focused in the key window of the active app), such as window.open after an await in the agent's click handler, opens as a background tab told to no session, never as a key popup window over the user's work. - tab.reload's wait after a wake uses the call's remaining time. - The dev driver mirrors the page-world, one-second window.
Merge-main commit by scripts/merge-main.sh. Merged by scripts/merge-main.sh: origin/main at 161e1cc. Resolved conflicts: - Resources/Localizable.xcstrings: xcstrings key-level union - cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py Merge-main-previous-head: 1bf6df0 Merge-main-base: 161e1cc
|
merge-gate: ci-status is not successful on 89c84cb. A fresh merge-override: comment from a write-access collaborator is required for: CLA Assistant, ci-status. For every check, name it and link a main run that fails the same check or write 'not on main', then add a real sentence explaining why it is safe. |
…callback budget The flaky watchdog test raced a 0 ms setTimeout against the next cell. When the timer's callback was queued on the session thread after the next cell became current but before that cell began, the watchdog counted the callback as the cell's work and never stopped it, so the cell timed out. The test now fires the timer by hand behind a held thread, which reproduces the race every time (red), and the idle case no longer waits on a real timer.
…at cell A cell became current when it was submitted, so a timer or event callback that the thread ran before reaching the cell was attributed to it. The watchdog exempts a run under the current cell from the callback budget, so a runaway callback queued ahead of a cell held the thread until that cell timed out. The session now marks a cell begun on the thread and attributes runs only to a begun cell.
The pending-timer test awaited a 1 ms timer after the flood and assumed the flood's callbacks had run; it now awaits a promise the flood's last callback settles, then schedules the full 10,000 timers again (none comes due), which proves every slot was released. The hung restore and the distant deadline in the tab-wake tests suspend until their task is cancelled instead of sleeping for an hour.
The plain Copy/Cut test and two Paste tests ran WebKit's commands against the real general pasteboard, so a broken redirect would have written or read the person's clipboard, and the Paste tests skipped their checks when the real clipboard's change count was low. They now run inside the stand-in, which also answers +generalPasteboard, and the Paste tests give the system change count explicitly so they always exercise the paste. The real general pasteboard is only looked up for identity and change counts, never read, written or cleared.
…17256) `cmux browser repl` is a persistent JavaScript REPL that agents use to drive cmux browser panes: Playwright page, locator, keyboard and mouse semantics with native trusted input, budgeted accessibility snapshots with diffs, tabs, cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and site tools. Guards live outside agent code: fill-only secrets with redaction and capture masking, a domain policy over every frame and fetch hop, a per-tab clipboard for session-created tabs, private per-session temp directories and bounded cells, fetches and timers. Agent work never moves the user's focus, hibernated tabs wake on use, and crashed tabs report how to recover. Squashed from #15570 (392 commits; the CLA action cannot read more than 250 commits of one pull request). Same tree as that branch's head.
…EPL (S0a) Three-way merge of main's browser REPL (3b15ee4, #17256) into the cmux-next copy. Base: #15570 at 3add499, the tree that b8c82aa moved (path-only edits confirmed by a diff). Main's changes come in; cmux-next's host adaptations stay. Runtime (cmux-tui/crates/cmux-browser-host/js): - Main's behavior and fixes: cell queue without a promise chain, per-cell cancel by id, cell console that drops a cancelled cell's late output, timer delay clamp and the per-session timer bound, output hard cap, session-private tmpdir for spill, exports and records, frozen host, driver and agent-tools hooks, __cmuxNative and the runtime namespace deleted before the first cell, fetch credentials (include, same-origin, omit), tab state (live, hibernated, waking, crashed), storageState and setStorageState scoped to the page's data store, IDN and trailing-dot host names in domain patterns, YouTube caption host check, page-clipboard.js, and the site tool changes (linear, jira, loader, webmcp drafts, auth-fill field checks, gmail, google docs/sheets/slides, notion, youtube). - Kept for cmux-next until the native ABI slice (S0b, decision D1): the host's secretSet/secretList/secretDelete/policyNarrow/policyGet/ policyLog/policyCheck natives, {__secret: name} handles, locator.fill with the handle in the page agent's fill call, narrow-only policy from the VM, host-side policy log and check, registrableDomain for sites, "stat" for lstatSync (the host has no lstat op yet). - One blocked-navigation event name, main's: navigation.blocked {targetId, url, reason}. The Rust host emits neither name today. Suite (tests/browser-parity): main's scenarios 34-39 and goldens, unit tests (tab-ownership, runtime, budget, mcp, agent-tools), sites tests (drafts and the rest), lib/test-dirs.mjs with its cleanup guard, lib/public-suffix.mjs, fixture-server and oracle changes, and the dev driver's tab ownership, data stores, hibernation and cookie scope. Every harness still builds its REPL through createHostedRepl (reference-host.mjs). The CLI backends now run each scenario in a new working directory. Not taken: main's native-boundary.mjs and its unit test (they implement main's ABI; they merge with reference-host.mjs into one oracle in S0b), and the comparison material that #16818 moved to the private repository (diff/, capabilities.json, reference/, chrome refs, references.mjs, reference-a sizes, parity-report.md, performance.md, reference-c-parity.md). Docs (plans/cmux-next/browser-repl): main's README, driver-protocol (Guards, hibernated and crashed tabs, native host contract), edge-cases and site-tools, with each cmux-next difference on a line that starts with "cmux-next:". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…EPL (S0a) Three-way merge of main's browser REPL (3b15ee4, #17256) into the cmux-next copy. Base: #15570 at 3add499, the tree that b8c82aa moved (path-only edits confirmed by a diff). Main's changes come in; cmux-next's host adaptations stay. Runtime (cmux-tui/crates/cmux-browser-host/js): - Main's behavior and fixes: cell queue without a promise chain, per-cell cancel by id, cell console that drops a cancelled cell's late output, timer delay clamp and the per-session timer bound, output hard cap, session-private tmpdir for spill, exports and records, frozen host, driver and agent-tools hooks, __cmuxNative and the runtime namespace deleted before the first cell, fetch credentials (include, same-origin, omit), tab state (live, hibernated, waking, crashed), storageState and setStorageState scoped to the page's data store, IDN and trailing-dot host names in domain patterns, YouTube caption host check, page-clipboard.js, and the site tool changes (linear, jira, loader, webmcp drafts, auth-fill field checks, gmail, google docs/sheets/slides, notion, youtube). - Kept for cmux-next until the native ABI slice (S0b, decision D1): the host's secretSet/secretList/secretDelete/policyNarrow/policyGet/ policyLog/policyCheck natives, {__secret: name} handles, locator.fill with the handle in the page agent's fill call, narrow-only policy from the VM, host-side policy log and check, registrableDomain for sites, and an early jira check of home.atlassian.com from the host's patterns (the host has no URL check op yet). lstatSync sends main's "lstat" op; the Rust host answers EINVAL for it until the fs rules slice (S1). - One blocked-navigation event name, main's: navigation.blocked {targetId, url, reason}. The Rust host emits neither name today. Suite (tests/browser-parity): main's scenarios 34-37 and goldens, unit tests (tab-ownership, runtime, budget, mcp, agent-tools), sites tests (drafts and the rest), lib/test-dirs.mjs with its cleanup guard, lib/public-suffix.mjs, fixture-server and oracle changes, and the dev driver's tab ownership, data stores, hibernation and cookie scope. Every harness still builds its REPL through createHostedRepl (reference-host.mjs). The CLI backends now run each scenario in a new working directory. Assertions on the host's guard texts stay cmux-next's (a refused secret names the frame URL; a TOTP test computes the code with reference-host's totp). The runtime's compact site list gains co.at and or.at. Not taken: main's native-boundary.mjs and its unit test, and scenarios 38-cookie-guards and 39-policy-rules (they need main's ABI and the cookie guards in the boundary; they come with the one-oracle merge in S0b), and the comparison material that #16818 moved to the private repository (diff/, capabilities.json, reference/, chrome refs, references.mjs, reference-a sizes, parity-report.md, performance.md, reference-c-parity.md). Docs (plans/cmux-next/browser-repl): main's README, driver-protocol (Guards, hibernated and crashed tabs, native host contract), edge-cases and site-tools, with each cmux-next difference on a line that starts with "cmux-next:". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t screens (#17230) * cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip) * cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip) * cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts * cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string) * cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons * cloud tab intro: title first, no icon * cloud tab intro: app icon banner back, lock badge while the plan needs pro * cloud tab intro: dark app icon in the banner * fix(cloud): wait for display helper readiness (#17132) * fix(cloud): explain unavailable display actions Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace. — unregistered * fix(cloud): explain unavailable display actions Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace. — unregistered * fix(cloud): reject cross-machine display opens before projection * fix(cloud): probe legacy desktop VMs for displays * fix(cloud): initialize wallpapers for new displays * fix(cloud): fail closed before display double-click opens * fix(cloud): replay repeated display ownership hints * test(cloud): cover display helper refresh * fix(cloud): refresh installed display helper * test(cloud): require display service readiness probe * fix(cloud): wait for display helper readiness * fix(cloud): keep display creation retryable * fix(cloud): authorize dynamic display ports * fix(cloud): let new display retry guest discovery * Revert "fix(cloud): let new display retry guest discovery" This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f. * test(cloud): sidebar visibility filter must keep display creation state Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds a failing regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): keep display creation state through the sidebar filter applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and dropped displayCreationMachines, staleMachineIDs and display memberships, so every desktop VM's New Display row reported additional displays as unavailable. Filter a copy instead so all per-machine state survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(surfaces): a split with no room opens as a tab in the target pane Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): optimistic display row; fall back to a tab when a split has no room New Display now shows a "Starting display…" row from the click until the guest answers, driven by the catalog's in-flight creation set. Opening a sidebar resource splits the focused pane; once split admission had no room left, the open failed with "Could not create the pane: noSpace". SurfacePaneFactory now opens it as a tab in the pane that would have been split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(surfaces): refused split is typed; sidebar gestures open a tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(surfaces): scope the no-room tab fallback to sidebar gestures The factory now throws a typed noSpace instead of opening a tab, so the layout replay and socket open verbs keep a truthful refused split. Sidebar opens and New Display use openPreferringSplit, which retries as a tab in the pane that would have been split. The empty Displays row no longer shows beside the optimistic Starting display row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): display creation needs no discovery round trip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(cloud): open the display pane at the click and skip pre-create discovery New Display paid two VM exec round trips (list, then create, about 2s each) before any pane appeared. The create reply is already the full guest catalog, so the list is dropped. The pane now opens at the click with a native Starting display state and is adopted by the projection once the guest assigns the display; creation failure closes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): harden the reserved display pane Drop a second click before it opens a pane; bind the reservation to the created display id so no other projection adopts it; keep creating when the pane cannot open; leave the display in the pool when the person closed the pane; show the failure on a pane the socket refuses to close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): additional displays serve noVNC beside the primary desktop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): make additional displays reachable on the VM private address Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::]. The client reaches every display through the private address, so each new display's first noVNC connection was refused and only recovered after a timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an older helper left on loopback, and drop the open-port call added for display ports: it changed no routing and cost a control-plane request, plus a desktop heal exec for display 1, on every open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): stale proxy match covers only this display's websockify Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): match stale display proxies by argv, not pgrep regex pgrep's ERE has no (?:...) groups, so the stale websockify match failed, the old loopback listener kept the port, and the rebound proxy exited. Read /proc argv for this user's websockify on this display's ports. Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on the private address, one proxy each, X sessions untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): a failed first desktop connection retries before failing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): retry a display's first connection; warm the carrier at create noVNC stops at Connect after an initial connect failure; its reconnect only follows a session that once connected. Restored and new displays could lose that race while the proxy or guest listener started, leaving "Failed to connect to server". Retry the route's first connection three times (0.5s/1s/2s on the injected clock) before showing the failure card. The first display open on a machine spent 12-18s starting its browser carrier after the guest exec. Start it alongside the create request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): an explicit Retry gets its own quiet first-connection retries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): run the guest display script test on the main actor CloudGuestDisplayScript is @MainActor; the test called it from a nonisolated context and broke the CmuxCloud package test build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239) * test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red) Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got per-runner roots: every build started cold and no seed could be adopted (job 111334766861). take-product-canonical-root.sh also refused a product built at a per-runner root on a fleet Mac without glaeda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): per-runner roots only on fleet Macs without glaeda canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every self-hosted runner without the glaeda helper, which included Blacksmith's ephemeral macOS runners: their builds started cold and their seeds were never adopted. Derive it only when the fleet directory exists. Let take-product-canonical-root.sh accept such a root when glaeda isn't there to hold it, so app-host test reruns build where the product was compiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207) * fix(ci): align owned-build-state guards with per-runner canonical roots #17168 (496195b) derives a canonical root per self-hosted runner, exports CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared /private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in tests/test_ci_owned_build_state.py still assumed the old contract, turning "CI fast guards" red on main. Update them to the new contract: the slot step exports the root, a per-runner root reads its own store, and a missing root reads nothing. The owned-state step now fails with a clear message when the root is missing, and rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the looser glob cannot map the store outside the Mac's package directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(l10n): translate the New Machine plan loading strings #17135 added machines.new.plan.loading, .retry and .error with the English text copied into every locale, so `localization_catalog.py check` reports 24 parity errors and "Fast static checks" fails on every PR. Translate them for all catalog locales; Retry reuses common.retry's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): repair cmuxTests compile after the sidebar reorder change #17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package, but RightSidebarTabCustomizationTests still relied on the app module exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&` in #expect, which the macro rejects ("operator can throw but expression is not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): satisfy dogfood-build runner guards and the Swift warning budget #17206 turned dogfood-build into a macOS build, which tripped three guards: no pinned Xcode, no fork branch in runs-on, and no static-preflight dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR branches ahead of the existing selector, and `needs: static-preflight`. The job's `if` already excludes forks, so manaflow-ai PRs keep the same runner. #17070's `RightSidebarModeBarDragController.coordinateSpace` is read from a Sendable geometry closure and broke the zero-warning budget; it is a plain String constant, so mark it `nonisolated`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): a root the glaeda hook exported wins over the per-runner root Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe refuses the root glaeda placed it in (runs 37157423969, 37155786981). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): honor the canonical root the glaeda hook exported #17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and exports exactly that, so the job then built somewhere glaeda does not hold, and main-compile-probe refused it: "glaeda placed this job in /private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda". Let any exported root win. A self-hosted job with no exported root still gets its per-runner root, so #17168's isolation for unplaced runners stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): an unplaced self-hosted job keeps the shared seeded root Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>, whose fingerprint never matches main's seeds, so new aws runners (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at 35 minutes in compile admission (run 37158561950, both attempts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): build unplaced self-hosted jobs at the shared seeded root #17168 gave every self-hosted job without an exported root its own /private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint, so main's DerivedData seeds and the owned build state never match it, and the prepare step clears it each job: every new aws runner compiled cold and hit the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on run 37158561950). Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still exports root N for the jobs it places, so those stay isolated. Unplaced concurrent runners on one Mac share the root again, as before #17168, until glaeda places those jobs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): the pool picker never picks an owned label it was not configured with Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per Mac) sorts before the minis' 26.6, so compile admission ran there and timed out at 35 minutes while the minis were idle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): pick only configured owned pools, not any owned-looking label simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners, five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR compile admission landed on contended EC2 runners and timed out at 35 minutes while the minis sat idle. Use only the configured pools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): the pool picker reads every page of organization runners Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has 509 runners and the first page holds the aws Macs and no idle mini, so the picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with 29 idle minis online). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): read every page of runners in the pool picker LiveState.runners read only the first page of 100 organization runners. manaflow-ai has about 500, and the first page holds the aws Macs but no idle mini, so the picker never saw the mini fleet: it took the aws 26.3 label while that was discoverable, and fell back to Blacksmith once it was not. Read up to ten pages. Against live data the picker now sees 38 online glaeda-std-xcode-26.6 minis (33 free) and picks them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: clear the Cloud sidebar warnings that broke the Swift warning budget Compile admission on a mini (run 37167089830) built cleanly but failed the zero-warning budget on three warnings from the Cloud sidebar work: `SurfaceCatalog.shared` used as a default argument of two @MainActor CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an implicit `self` in CloudTreeOutlineView's machine-lift reopen closure. Resolve `.shared` inside the main-actor body and make the capture explicit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): preserve canonical root and translation wording * test(ci): align canonical root guard with main behavior * test: use isolated catalogs in Cloud sidebar fixtures Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated. Co-authored-by: Austin Wang <austinwang115@gmail.com> * test: model loading Cloud sidebar state Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption. Co-authored-by: Austin Wang <austinwang115@gmail.com> * test: use a terminal panel for cloud directory settings The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata. Co-authored-by: Austin Wang <austinwang115@gmail.com> * fix(cloud): restore device directory fallback lost in main merge The merge of main took #17107's presentation file and dropped the fallback from 27a8d804fdf, so a device projection whose catalog row is still restoring rendered no directory and SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory failed on b6ea6632cb1 (the regression run is that CI failure). Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> * ci: require a written merge override for red CI (#17217) * ci: require merge-gate before merges * Add API-only merge gate for pull requests * test: cover merge-gate override decisions * ci: wire merge-gate tests and push timing * ci: pin merge gate workflow source * Reject read-only override authors * ci: harden merge gate runner and author checks * ci: keep merge gate on hosted capacity * ci: fail closed when collaborator lookup fails * ci: reject stale successes during reruns * ci: fail closed for untimestamped queued checks * ci: fail closed for untimestamped pending statuses * ci: verify matching override failure evidence * test: cover mismatched override failure evidence * ci: choose newest merge gate check run * ci: require compile evidence for main-fix merges (#16993) * test: reproduce main-fix merging without compile evidence The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: require build evidence for main fixes gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal. CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci: harden merge gate freshness and evidence * ci: require per-check override reasons * ci: complete merge gate review fixes * docs(ci): document merge-gate rollout order * ci: make merge helper rollout repairable * ci: point merge helper refusals to repair runbook * ci: resolve merge helper symlink for main fixes * docs: remove stale main-fix PR reference * fix: avoid pipefail false negatives in merge helper * fix: keep merge gate events fresh per pull request --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Share one vCPU and memory pool across a plan's Cloud VMs (#17238) * test(vms): reproduce missing shared Cloud VM resource pool * feat(vms): share one vCPU and memory pool across a plan's Cloud VMs Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row. Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row. The repository enforces the pool next to the active-VM count, under the same transaction and billing lock, on create, Base open/reset, paused resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy now describe pooled resources. * Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings * test(vms): reproduce leaked compute claim and stale-plan resume pool * fix(vms): give back unused compute claims and resume against the caller's pool * docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale * Cloud VM: snapshot create honors Idempotency-Key (#17244) * test(cloud-vm): snapshot create dedups by idempotency key (red) A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return the first snapshot and take no second one; a retry while the first runs is refused as in progress; the same key with another name is a conflict; a failed attempt frees the key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud-vm): snapshot create dedups by Idempotency-Key POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table, cloud_vm_snapshot_requests (one row per machine and key, additive migration 20261004120000), records a pending attempt before the provider call and the provider snapshot after it. A retry with the same key returns the first snapshot and records no second usage event; a retry while the first runs gets 409 vm_snapshot_in_progress (retryable); the same key with another name gets 409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending row older than 15 minutes (route budget 600 s) is taken over by a retry. Requests without a key behave as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency With --create, takes one snapshot of the throwaway smoke machine twice with the same Idempotency-Key, requires the same snapshotId, deletes that snapshot, then destroys the machine as before. No fixed VM id and no printed token: the smoke mints its own throwaway user session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Preserve selected tab when closing another surface (#16645) * Preserve selected tab when closing another surface * Only select the closing pane's tab when that pane is focused BonsplitController.selectTab also focuses the pane, so calling it for an unfocused pane moved focus into the pane where a tab closed, the focus theft this change is meant to stop. Bonsplit already keeps the selection when an unselected tab closes; the shouldCloseTab change is the actual fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs: detail tmux help options (#16780) Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * test: guard managed contributor difficulty labels (#16448) * test: guard managed contributor difficulty labels * test: reject invalid difficulty descriptions * test: exercise difficulty label sync requests --------- Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250) * test(cloud): expect the display helper readiness loop #17132 replaced the one-line `list || exit 1` probe with a bounded retry loop and updated the package test, but cmuxTests still asserted the old line, so CloudDisplayCatalogTests.guestCommandShape failed on main (run 37186487936, shard 4/7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): make the restored-display retry test a desktop #17132's initialDesktopFailureRetries configured port 6902 without a resource ID, so CloudBrowserAccessState.isDesktop was false and desktopConnectionDidChange ignored the failure: no quiet retry, and the following navigations[1] trapped and crashed the shard 5 app host on main (run 37186487936). Give it the .display identity additional displays carry and require the retry before indexing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): a foreign display click shows one synchronous hint #17103 made a display click on another machine's workspace reject synchronously through showDisplayOpenHint, so no tree operation starts and waitForOpen() waited out the 60 s suite limit on main (run 37186487936, shards 2 and 3). Assert no operation ran and that the rejection is shown once; this stays red until the double-click stops repeating the hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): show a foreign display's ownership hint once per double-click AppKit delivers a double-click's first click to handleSingleClick, which already shows the ownership hint, and #17103 also showed it from handleDoubleClick, so the user got the same rejection twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): never show upstream tree failures in the Machines status row #17103 started presenting treeError verbatim so its ownership hints would survive, but treeError also carries raw upstream failures (LocalizedError descriptions, create output), so a URL with query parameters reached the toolbar text, hover help and copy menu. MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it on main (run 37186487936, shard 7). Hints now travel through their own onHint sink (falling back to onFailure for other callers), the panel records them as trusted copy, and the status row shows the tree error verbatim only when it is that hint; everything else gets the safe recovery message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): keep merge-gate diagnostics on the exact PR (#17248) * fix: keep merge-gate diagnostics on the exact PR * fix: print repair guidance for gate diagnostics * fix: allow merge-gate PR comments when permitted * fix: reject mismatched merge-gate event identities * fix: keep merge-gate alive on comment errors * fix: keep the cmux-cua credential out of the Codex argv (#17252) * test: codex wrapper must not put the cmux-cua credential in argv The Codex wrapper passes the cmux-cua socket credential as -c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any local user can read it with ps. These assertions require the value to be absent from argv, Codex to receive it in its environment, and the MCP server to receive it through env_vars. The fake Codex now builds the MCP server environment the way Codex does (allow-list + env_vars + env). * fix: keep the cmux-cua credential out of the Codex argv The wrapper passed the socket credential as a Codex -c env override, which put the value in the codex process argv where any local user can read it with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards the variable by name to the MCP server (Codex env_vars, openai/codex#5246). Codex's default shell_environment_policy excludes *TOKEN* names from agent shell commands. Attachment stays fail-closed when no credential resolves. * fix: keep codex wrapper overrides when the subcommand gets -c (#17257) * test: codex wrapper overrides must survive subcommand -c flags Codex declares -c/--config, --enable, and --disable as clap global arguments. When a subcommand such as exec or resume also receives one, Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's cmux-cua MCP config, hook config, and --disable computer_use placed before the subcommand are dropped. The fake Codex now applies that rule, and new cases cover exec, resume, mixed root and subcommand -c, and a literal prompt after --. * fix: keep codex wrapper overrides when the subcommand gets -c Codex declares -c/--config, --enable, and --disable as clap global arguments and keeps only the subcommand-level values when a subcommand also receives one. The wrapper put its cmux-cua MCP config, hook config, and --disable computer_use before the user's argv, so codex exec -c ... or codex resume ... -c ... dropped all of them. The wrapper now moves the user's subcommand-level global arguments, in order, in front of the subcommand. Codex reads one root-level list with the same precedence as before. Tokens after -- stay in place, and interactive launches without a subcommand are unchanged. * Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256) `cmux browser repl` is a persistent JavaScript REPL that agents use to drive cmux browser panes: Playwright page, locator, keyboard and mouse semantics with native trusted input, budgeted accessibility snapshots with diffs, tabs, cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and site tools. Guards live outside agent code: fill-only secrets with redaction and capture masking, a domain policy over every frame and fetch hop, a per-tab clipboard for session-created tabs, private per-session temp directories and bounded cells, fetches and timers. Agent work never moves the user's focus, hibernated tabs wake on use, and crashed tabs report how to recover. Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the CLA action cannot read more than 250 commits of one pull request). Same tree as that branch's head. * Cloud: VM file operation routes (port of #16936) (#17254) * Cloud: VM file operation routes (port of #16936), missing path answers 404 Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936 (feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat, write, mkdir, remove) with the Freestyle driver and gateway methods. Includes the fix from feat-cmux-next: Freestyle removes a missing path with success, so removeVmFile stats first and answers a missing file with 404 vm_file_not_found (any other stat failure, a missing VM included, stays a provider failure). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found The staging rehearsal of #17254 showed stat of a removed file answering 502 vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as remove already did, and title the error 'File not found'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop calling the legacy Subrouter during account deletion (#17273) * test: account deletion must not call the retired legacy Subrouter Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop calling the legacy Subrouter during account deletion The legacy Subrouter at subrouter.cmux.dev is being retired. Account deletion now skips the legacy revoke phase and needs no legacy env vars. Local mapping rows are still deleted, and a legacy_delete_pending tombstone from an older deployment resumes at the hosted checkpoint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Split the account DELETE handler under the complexity limit DELETE delegates to deleteAccount and named phase helpers that share one progress record, so its complexity drops from 46 to under 20 and its grandfathered baseline entry is removed. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: remove merge gate and restore exact-head merging (#17275) * Cloud: private network routes (port of #16948) (#17255) * Cloud: private network routes (port of #16948), missing firewall rule answers 404 Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948 (feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete), /api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle driver, gateway and private-network workflow pieces. Includes the fix from feat-cmux-next: a firewall rule that is not in the caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race on delete too); a missing VM endpoint stays vm_not_found. Stacked on the file-routes port (#17254). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint No behavior change; makes the parser testable without the route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints The staging rehearsal of #17255 found two defects. validCidr compared the prefix with net.isIP(), which returns the family (4 or 6), so any IPv4 prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership check looked the VM up in the personal scope, but every new VM is team-owned, so vmId endpoints were vm_not_found; the route now resolves the account scope when a vmId is named, and the VM must be the caller's own (the firewall edits the caller's network). The provider now gets only the rule fields: the Freestyle driver spreads its input into the request body, so userId and provider were sent to Freestyle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): decide firewall rule ownership on the shared provider account The Freestyle account is shared by every cmux user, so the API decides whose a rule is. Reading and deleting: the rule names at least one resource and every resource it names is the caller's. get and delete now read the rule by id; they searched only the network listing, so a rule that named a VM and a CIDR was created (201) and then could not be found or deleted. The list merges the network listing with one listing per caller VM. Creating: the destination must be a caller resource (400 vm_invalid_firewall_rule), because a destination of only an address range or the public Internet would reach other tenants' machines. Every firewall call resolves the account scope like the other VM routes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs Security review P2: the provider adds selectors as new optional fields, and an unknown one could name another tenant's resource, so a rule with an unknown endpoint field is not the caller's. Review P3: send the canonical range so a valid non-canonical CIDR does not fail at the provider. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found' Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations These routes are new on a provider account shared by every cmux user. Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An unfiltered list reads the network plus at most the 10 newest live VMs, one provider call each; older VMs list with ?vmId. Create and delete are throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID (no other VM mutation route has a limiter, so this follows the team-invite limiter: fail closed when the firewall is unavailable, fail open and report when the rule is unset or removed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897) * remote-tmux: failing test for a torn-down stream feeding the next one A control stream torn down for a reconnect keeps delivering what its reader had already buffered. The test holds the main actor while a first client writes 560 KB, starts a reconnect, and expects none of those bytes to reach the connection. * remote-tmux: stop a torn-down stream from feeding the next one Cancelling the task that reads a control client's stdout does not empty the reader's buffer, so chunks the old client had already written were still ingested after the teardown. Once the reconnect had respawned, a leftover command result was taken for the new client's attach reply. The connection then never asked for windows and the mirror stayed blank for good. Each read loop now stops as soon as its process generation is no longer the current one, and closes its reader. --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> * remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237) * remote-tmux: failing test for a docked terminal closed when its window's mirrors move * remote-tmux: keep a window whose Dock has panels when its mirrors move out --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> * Expose the workspace task-status lane to custom sidebars (#17245) Custom sidebars could not read a workspace's task-status lane. cmux already resolves one per workspace and the control socket can pin it, but the interpreter data context carried no field for it, so a sidebar had no way to group or colour rows by whether a workspace needs attention. `workspaces[i].status` now carries the resolved lane as its raw wire value: todo, working, needs-attention, review or done. The snapshot takes it as a required parameter so a dropped wiring breaks the build rather than reporting a silent "todo". Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Clear restored agent notifications once the agent is gone (#17067) * test: prune read notifications of agents that died with the previous app An agent alive at quit dies without SessionEnd, so its last "Completed in" notification is restored on every launch and shown as the workspace's latest sidebar summary even though no agent is running. The stale-agent sweep must drop it once the pane has no agent again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: drop restored agent notifications once the agent does not return Notifications persist across relaunch so an unseen agent result is not lost, but an agent that was alive when cmux quit dies without SessionEnd. Nothing clears its notification afterwards: agent PIDs are not restored, so the 30s stale-PID sweep has no dead PID to catch. Restore now records the notifications of local panes that hosted an agent (resume binding or restorable agent snapshot). The stale-agent sweep removes the read ones when the pane has no agent PID again; unread ones survive until read, and a pane the agent resumes into is handed back to its hooks. Remote terminals are skipped since their agent can outlive the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: keep restored notifications while the resume is in flight Covers a read notification posted after restore (never tracked) and a pane whose restored resume has not reported an agent PID yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: defer restored notification prune while the resume is in flight The 30-second sweep can run before an auto-resumed agent reports its PID. Skip panes whose restored command is still in flight, using the coordinator's existing ownsInFlightRestoredCommand contract. Also look up tracked notifications by id instead of scanning the store per panel, and mark the value-only snapshot helper nonisolated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: keep read non-agent notifications on a restored agent pane The restore tracks every notification persisted on a pane that hosted an agent, so a read `cmux notify` banner on that pane is pruned with the agent's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: track only agent-produced notifications on restored agent panes Use the persisted `isAgentEvent` provenance instead of panel ownership so a `cmux notify` banner on a pane that hosted an agent is not retired with the agent's result. Unknown provenance restores as agent-produced, matching TerminalNotificationStore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Send sidebar links through the external-open rules (#7397) * browser: apply external-open rules to sidebar links The sidebar's pull-request and port links (SwiftUI and AppKit rows, and the open-all-pull-requests action) opened in the embedded browser whenever that preference was on, without consulting the URL rules that route a site to the system browser. Sites listed there cannot work in the embedded web view at all, so a rule now wins over the embedded preference on those paths, the same way it does for a click inside a page. * browser: require a user event before a link escapes to the system browser WebKit reports a script calling click() on an anchor as .linkActivated, the same as a real click, so the external-open rules on their own let a page hand itself a system-browser open at a moment of its choosing. The navigation-typed escape now also requires an AppKit event in flight (a key, left-mouse, or middle-mouse event) and never intercepts a download, on every path that consults the rules: the main navigation delegate, the target=_blank UI delegate, and both popup delegates. The context menu's Open Link in New Tab is a gesture by construction and says so. The event check is a bound rather than a proof: NSApp.currentEvent says an event is being dispatched, not that this navigation is the thing the user asked for. Middle-clicks arrive as otherMouse events and count. * browser: e2e coverage for external-open link routing BrowserExternalOpenRoutingUITests drives real WebKit link activations through the socket browser.click against a local fixture and asserts routing at the delegate layer, where popup-vs-link-activation behavior actually diverges and unit tests cannot reach. Escapes are captured to a file through the existing DEBUG-only UI-test sink (CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation handler's default opener, so CI never opens Safari. Four cases: a matched link click escapes while the embedded page stays put; an unmatched click navigates embedded; a scripted window.open to a matched host never escapes; a target=_blank form POST to a matched host stays embedded. The shared BrowserFixtureSocketTestCase gains subclass hooks for launch arguments and environment, falls back from the in-process socket client to nc -U and then the bundled cmux CLI, disables hidden-webview discarding for the backgrounded UI-test host, and polls browser.wait through the cold-start content-process transient. * browser: click links for real in the external-open UI tests A link now leaves for the system browser only while a real input event is in flight, and the socket browser.click runs JavaScript, so the matched and unmatched link cases click through accessibility the way a person does. The scripted popup and form cases keep the socket click, since a scripted action is what they test. * browser: keep only the sidebar links, drop the in-page activation change The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher. * browser: use a rule the pattern safety check accepts in the port-link test --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci: require merge checks only when their workflows exist (#17284) * test: cover merging repos without aggregate CI workflow * fix: make merge checks conditional on base workflows * ci: fall back to ancestor evidence for --main-fix (#17277) * ci: use nearest ancestor for main-fix evidence * ci: constrain ancestor evidence to path-filtered changes * ci: inspect renamed paths in ancestor evidence * ci: bound ancestor evidence traversal * ci: reject incomplete ancestor path comparisons * fix(session): sweep stale scrollback replay files (#16056) * test(session): cover replay sweep and permissions Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): sweep stale scrollback replay files Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): sweep stale replay files before restore Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): remove synchronous replay sweep from app init Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): gate restore on off-main replay cleanup Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(app): keep replay sweep off startup critical path Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): skip replay sweep under XCTest * fix(session): harden retained replay files during stale sweep * docs(session): keep crash-recovery gate semantics accurate * test(session): cover legacy replay permissions and sweep filters --------- Signed-off-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Cover dotted Claude project dir in session directory search (#4939) * docs: clarify Claude project dir decode asymmetry * test: cover dotted Claude project dir in session directory scope * test: scope Claude session roots per task instead of process env Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree. * test: cover Claude cwd-filter lookup without a DEBUG seam Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded. --------- Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Answer the tmux session commands Claude Code calls (#13632) * Answer the tmux session commands Claude Code calls Claude Code's tmux backend tears down and reattaches its agent panes with kill-session, switch-client, new-session -A and show-options -g prefix. The compatibility layer rejected all four, so a Teams session failed with "Unsupported tmux compatibility command" once it got past creating panes. A tmux session is a cmux workspace, which has-session and new-session already assume, so kill-session closes that workspace, switch-client selects it, and new-session -A attaches to it when it exists instead of creating a duplicate. show-options now answers from a table, and prefix reports the C-b that a default tmux client would. The sequence test drives all four through the real shim. Its fake socket also now unwraps the capability envelope that the shell integration adds inside a cmux terminal, so the test reports the behavior it checks rather than a JSON decode error; that unwrapping moved into the shared helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Reject a socket payload that is not a request The fake servers indexed request["method"] straight off json.loads, so a payload that decoded to null, a list, or an object with a non-string method raised inside the handler thread and surfaced as an unrelated CLI error. They now answer those with an error line, which names the real problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: reject empty fake socket request methods Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: reject a failed tmux session lookup and kill-session -a A workspace.list error must not become a new session, and kill-session -a must not close the caller. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep a failed tmux session lookup from creating a workspace new-session -A treated every resolution error as a missing session, and kill-session ignored -a and closed the caller. A missing session still creates; a lookup failure and an unsupported flag now fail first. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974) * Add failing test: sidebar nil-comparison yields nothing (#7943) In a custom sidebar, `x != nil` / `x == nil` against a bound optional field does not evaluate to true/false — it evaluates to nothing. Interpolation renders empty, ternaries always take the else branch, and `if x != nil` guards are never taken, so optional-guarded views never render. This commit adds only the regression test (no fix) so CI shows it red. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix sidebar nil-comparison to evaluate to a Bool (#7943) The interpreter's value model had no null case and no `nil`-literal evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the same value that means "expression unsupported / no value". `evalInfix` then bailed on the comparison, so `x != nil` / `x == nil` produced nothing: interpolation rendered empty, ternaries always took the else branch, and `if x != nil` guards were never taken. Optional-guarded views (including the shipped status-board.swift / finder.swift examples) silently drew nothing. - Add `SwiftValue.null` for the `nil` literal and for comparing an absent optional field against `nil` (distinct from host `nil` = "no value"). - Evaluate `NilLiteralExprSyntax` to `.null`. - Handle `==` / `!=` before the operand guards, coalescing an absent operand (host `nil`) and the `nil` literal to `.null`, so the comparison yields a Bool that is true/false when present and false/true when absent. Fixes #7943 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: preserve nil comparison evaluation failures * fix: preserve nested nil comparison misses * fix: preserve parenthesized nil comparison misses * fix: handle nil optional binding and equality budget --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> * fix: refresh merge helper and honor neutral checks (#17294) * test: cover neutral checks and helper checkout refresh * test: tolerate absent git diagnostics * fix: refresh clean main checkout before merging * test: cover cloud welcome close shortcut ownership * fix: route cloud welcome close shortcut to its window --------- Signed-off-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Leo <cheerleaderleo@outlook.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: BlueRaddish <jeeholife2@gmail.com> Co-authored-by: EJ <ej@campbell.name> Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: Philipp Mochine <philipp@mochine.de> Co-authored-by: mys <wowpotato@naver.com> Co-authored-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Sungho Park <relilau00@gmail.com> Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mark Xian <mark-xian@foxmail.com> Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
…visible (#17127) * cloud: enable cloud for pro, upgrade for free, keep the button when the panel rebuilds the cloud tab and settings > cloud now show enable cloud only when the plan includes cloud and upgrade for free accounts. the plan answer lives on the account flow (per account) instead of the panel's view state, so switching sidebar modes no longer drops the button back to "checking your cmux plan…". * cloud: stop waiting on a slow plan check, title enable cloud machines, debug plan override * cloud: cloud.fill for the pro-required screen, settings shows only the enable row until cloud is on, enable cloud machines title * cloud: drop the debug plan override * cloud: keep prominent cloud buttons visible in an inactive window, keep a known plan through failed or cancelled checks, recheck settings on account change * cloud gate: newest plan check wins, drop answers for a switched account * fix cloud billing plan concurrency and state ownership * fix cloud plan localization parity * fix cloud entitlement detection for team plans * avoid stale cloud entitlements across team changes * report billing refresh success to settings * preserve account flow billing refresh contract * cloud onboarding: introducing cmux cloud welcome, and cloud enablement screens (#17230) * cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip) * cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip) * cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts * cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string) * cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons * cloud tab intro: title first, no icon * cloud tab intro: app icon banner back, lock badge while the plan needs pro * cloud tab intro: dark app icon in the banner * fix(cloud): wait for display helper readiness (#17132) * fix(cloud): explain unavailable display actions Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace. — unregistered * fix(cloud): explain unavailable display actions Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace. — unregistered * fix(cloud): reject cross-machine display opens before projection * fix(cloud): probe legacy desktop VMs for displays * fix(cloud): initialize wallpapers for new displays * fix(cloud): fail closed before display double-click opens * fix(cloud): replay repeated display ownership hints * test(cloud): cover display helper refresh * fix(cloud): refresh installed display helper * test(cloud): require display service readiness probe * fix(cloud): wait for display helper readiness * fix(cloud): keep display creation retryable * fix(cloud): authorize dynamic display ports * fix(cloud): let new display retry guest discovery * Revert "fix(cloud): let new display retry guest discovery" This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f. * test(cloud): sidebar visibility filter must keep display creation state Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds a failing regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): keep display creation state through the sidebar filter applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and dropped displayCreationMachines, staleMachineIDs and display memberships, so every desktop VM's New Display row reported additional displays as unavailable. Filter a copy instead so all per-machine state survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(surfaces): a split with no room opens as a tab in the target pane Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): optimistic display row; fall back to a tab when a split has no room New Display now shows a "Starting display…" row from the click until the guest answers, driven by the catalog's in-flight creation set. Opening a sidebar resource splits the focused pane; once split admission had no room left, the open failed with "Could not create the pane: noSpace". SurfacePaneFactory now opens it as a tab in the pane that would have been split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(surfaces): refused split is typed; sidebar gestures open a tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(surfaces): scope the no-room tab fallback to sidebar gestures The factory now throws a typed noSpace instead of opening a tab, so the layout replay and socket open verbs keep a truthful refused split. Sidebar opens and New Display use openPreferringSplit, which retries as a tab in the pane that would have been split. The empty Displays row no longer shows beside the optimistic Starting display row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): display creation needs no discovery round trip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(cloud): open the display pane at the click and skip pre-create discovery New Display paid two VM exec round trips (list, then create, about 2s each) before any pane appeared. The create reply is already the full guest catalog, so the list is dropped. The pane now opens at the click with a native Starting display state and is adopted by the projection once the guest assigns the display; creation failure closes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): harden the reserved display pane Drop a second click before it opens a pane; bind the reservation to the created display id so no other projection adopts it; keep creating when the pane cannot open; leave the display in the pool when the person closed the pane; show the failure on a pane the socket refuses to close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): additional displays serve noVNC beside the primary desktop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): make additional displays reachable on the VM private address Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::]. The client reaches every display through the private address, so each new display's first noVNC connection was refused and only recovered after a timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an older helper left on loopback, and drop the open-port call added for display ports: it changed no routing and cost a control-plane request, plus a desktop heal exec for display 1, on every open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): stale proxy match covers only this display's websockify Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): match stale display proxies by argv, not pgrep regex pgrep's ERE has no (?:...) groups, so the stale websockify match failed, the old loopback listener kept the port, and the rebound proxy exited. Read /proc argv for this user's websockify on this display's ports. Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on the private address, one proxy each, X sessions untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): a failed first desktop connection retries before failing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): retry a display's first connection; warm the carrier at create noVNC stops at Connect after an initial connect failure; its reconnect only follows a session that once connected. Restored and new displays could lose that race while the proxy or guest listener started, leaving "Failed to connect to server". Retry the route's first connection three times (0.5s/1s/2s on the injected clock) before showing the failure card. The first display open on a machine spent 12-18s starting its browser carrier after the guest exec. Start it alongside the create request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): an explicit Retry gets its own quiet first-connection retries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): run the guest display script test on the main actor CloudGuestDisplayScript is @MainActor; the test called it from a nonisolated context and broke the CmuxCloud package test build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239) * test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red) Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got per-runner roots: every build started cold and no seed could be adopted (job 111334766861). take-product-canonical-root.sh also refused a product built at a per-runner root on a fleet Mac without glaeda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): per-runner roots only on fleet Macs without glaeda canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every self-hosted runner without the glaeda helper, which included Blacksmith's ephemeral macOS runners: their builds started cold and their seeds were never adopted. Derive it only when the fleet directory exists. Let take-product-canonical-root.sh accept such a root when glaeda isn't there to hold it, so app-host test reruns build where the product was compiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207) * fix(ci): align owned-build-state guards with per-runner canonical roots #17168 (496195b) derives a canonical root per self-hosted runner, exports CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared /private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in tests/test_ci_owned_build_state.py still assumed the old contract, turning "CI fast guards" red on main. Update them to the new contract: the slot step exports the root, a per-runner root reads its own store, and a missing root reads nothing. The owned-state step now fails with a clear message when the root is missing, and rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the looser glob cannot map the store outside the Mac's package directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(l10n): translate the New Machine plan loading strings #17135 added machines.new.plan.loading, .retry and .error with the English text copied into every locale, so `localization_catalog.py check` reports 24 parity errors and "Fast static checks" fails on every PR. Translate them for all catalog locales; Retry reuses common.retry's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): repair cmuxTests compile after the sidebar reorder change #17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package, but RightSidebarTabCustomizationTests still relied on the app module exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&` in #expect, which the macro rejects ("operator can throw but expression is not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): satisfy dogfood-build runner guards and the Swift warning budget #17206 turned dogfood-build into a macOS build, which tripped three guards: no pinned Xcode, no fork branch in runs-on, and no static-preflight dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR branches ahead of the existing selector, and `needs: static-preflight`. The job's `if` already excludes forks, so manaflow-ai PRs keep the same runner. #17070's `RightSidebarModeBarDragController.coordinateSpace` is read from a Sendable geometry closure and broke the zero-warning budget; it is a plain String constant, so mark it `nonisolated`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): a root the glaeda hook exported wins over the per-runner root Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe refuses the root glaeda placed it in (runs 37157423969, 37155786981). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): honor the canonical root the glaeda hook exported #17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and exports exactly that, so the job then built somewhere glaeda does not hold, and main-compile-probe refused it: "glaeda placed this job in /private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda". Let any exported root win. A self-hosted job with no exported root still gets its per-runner root, so #17168's isolation for unplaced runners stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): an unplaced self-hosted job keeps the shared seeded root Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>, whose fingerprint never matches main's seeds, so new aws runners (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at 35 minutes in compile admission (run 37158561950, both attempts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): build unplaced self-hosted jobs at the shared seeded root #17168 gave every self-hosted job without an exported root its own /private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint, so main's DerivedData seeds and the owned build state never match it, and the prepare step clears it each job: every new aws runner compiled cold and hit the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on run 37158561950). Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still exports root N for the jobs it places, so those stay isolated. Unplaced concurrent runners on one Mac share the root again, as before #17168, until glaeda places those jobs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): the pool picker never picks an owned label it was not configured with Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per Mac) sorts before the minis' 26.6, so compile admission ran there and timed out at 35 minutes while the minis were idle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): pick only configured owned pools, not any owned-looking label simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners, five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR compile admission landed on contended EC2 runners and timed out at 35 minutes while the minis sat idle. Use only the configured pools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ci): the pool picker reads every page of organization runners Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has 509 runners and the first page holds the aws Macs and no idle mini, so the picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with 29 idle minis online). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): read every page of runners in the pool picker LiveState.runners read only the first page of 100 organization runners. manaflow-ai has about 500, and the first page holds the aws Macs but no idle mini, so the picker never saw the mini fleet: it took the aws 26.3 label while that was discoverable, and fell back to Blacksmith once it was not. Read up to ten pages. Against live data the picker now sees 38 online glaeda-std-xcode-26.6 minis (33 free) and picks them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: clear the Cloud sidebar warnings that broke the Swift warning budget Compile admission on a mini (run 37167089830) built cleanly but failed the zero-warning budget on three warnings from the Cloud sidebar work: `SurfaceCatalog.shared` used as a default argument of two @MainActor CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an implicit `self` in CloudTreeOutlineView's machine-lift reopen closure. Resolve `.shared` inside the main-actor body and make the capture explicit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): preserve canonical root and translation wording * test(ci): align canonical root guard with main behavior * test: use isolated catalogs in Cloud sidebar fixtures Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated. Co-authored-by: Austin Wang <austinwang115@gmail.com> * test: model loading Cloud sidebar state Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption. Co-authored-by: Austin Wang <austinwang115@gmail.com> * test: use a terminal panel for cloud directory settings The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata. Co-authored-by: Austin Wang <austinwang115@gmail.com> * fix(cloud): restore device directory fallback lost in main merge The merge of main took #17107's presentation file and dropped the fallback from 27a8d804fdf, so a device projection whose catalog row is still restoring rendered no directory and SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory failed on b6ea6632cb1 (the regression run is that CI failure). Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> * ci: require a written merge override for red CI (#17217) * ci: require merge-gate before merges * Add API-only merge gate for pull requests * test: cover merge-gate override decisions * ci: wire merge-gate tests and push timing * ci: pin merge gate workflow source * Reject read-only override authors * ci: harden merge gate runner and author checks * ci: keep merge gate on hosted capacity * ci: fail closed when collaborator lookup fails * ci: reject stale successes during reruns * ci: fail closed for untimestamped queued checks * ci: fail closed for untimestamped pending statuses * ci: verify matching override failure evidence * test: cover mismatched override failure evidence * ci: choose newest merge gate check run * ci: require compile evidence for main-fix merges (#16993) * test: reproduce main-fix merging without compile evidence The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: require build evidence for main fixes gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal. CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci: harden merge gate freshness and evidence * ci: require per-check override reasons * ci: complete merge gate review fixes * docs(ci): document merge-gate rollout order * ci: make merge helper rollout repairable * ci: point merge helper refusals to repair runbook * ci: resolve merge helper symlink for main fixes * docs: remove stale main-fix PR reference * fix: avoid pipefail false negatives in merge helper * fix: keep merge gate events fresh per pull request --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Share one vCPU and memory pool across a plan's Cloud VMs (#17238) * test(vms): reproduce missing shared Cloud VM resource pool * feat(vms): share one vCPU and memory pool across a plan's Cloud VMs Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row. Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row. The repository enforces the pool next to the active-VM count, under the same transaction and billing lock, on create, Base open/reset, paused resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy now describe pooled resources. * Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings * test(vms): reproduce leaked compute claim and stale-plan resume pool * fix(vms): give back unused compute claims and resume against the caller's pool * docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale * Cloud VM: snapshot create honors Idempotency-Key (#17244) * test(cloud-vm): snapshot create dedups by idempotency key (red) A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return the first snapshot and take no second one; a retry while the first runs is refused as in progress; the same key with another name is a conflict; a failed attempt frees the key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud-vm): snapshot create dedups by Idempotency-Key POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table, cloud_vm_snapshot_requests (one row per machine and key, additive migration 20261004120000), records a pending attempt before the provider call and the provider snapshot after it. A retry with the same key returns the first snapshot and records no second usage event; a retry while the first runs gets 409 vm_snapshot_in_progress (retryable); the same key with another name gets 409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending row older than 15 minutes (route budget 600 s) is taken over by a retry. Requests without a key behave as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency With --create, takes one snapshot of the throwaway smoke machine twice with the same Idempotency-Key, requires the same snapshotId, deletes that snapshot, then destroys the machine as before. No fixed VM id and no printed token: the smoke mints its own throwaway user session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Preserve selected tab when closing another surface (#16645) * Preserve selected tab when closing another surface * Only select the closing pane's tab when that pane is focused BonsplitController.selectTab also focuses the pane, so calling it for an unfocused pane moved focus into the pane where a tab closed, the focus theft this change is meant to stop. Bonsplit already keeps the selection when an unselected tab closes; the shouldCloseTab change is the actual fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs: detail tmux help options (#16780) Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * test: guard managed contributor difficulty labels (#16448) * test: guard managed contributor difficulty labels * test: reject invalid difficulty descriptions * test: exercise difficulty label sync requests --------- Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250) * test(cloud): expect the display helper readiness loop #17132 replaced the one-line `list || exit 1` probe with a bounded retry loop and updated the package test, but cmuxTests still asserted the old line, so CloudDisplayCatalogTests.guestCommandShape failed on main (run 37186487936, shard 4/7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): make the restored-display retry test a desktop #17132's initialDesktopFailureRetries configured port 6902 without a resource ID, so CloudBrowserAccessState.isDesktop was false and desktopConnectionDidChange ignored the failure: no quiet retry, and the following navigations[1] trapped and crashed the shard 5 app host on main (run 37186487936). Give it the .display identity additional displays carry and require the retry before indexing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): a foreign display click shows one synchronous hint #17103 made a display click on another machine's workspace reject synchronously through showDisplayOpenHint, so no tree operation starts and waitForOpen() waited out the 60 s suite limit on main (run 37186487936, shards 2 and 3). Assert no operation ran and that the rejection is shown once; this stays red until the double-click stops repeating the hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): show a foreign display's ownership hint once per double-click AppKit delivers a double-click's first click to handleSingleClick, which already shows the ownership hint, and #17103 also showed it from handleDoubleClick, so the user got the same rejection twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): never show upstream tree failures in the Machines status row #17103 started presenting treeError verbatim so its ownership hints would survive, but treeError also carries raw upstream failures (LocalizedError descriptions, create output), so a URL with query parameters reached the toolbar text, hover help and copy menu. MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it on main (run 37186487936, shard 7). Hints now travel through their own onHint sink (falling back to onFailure for other callers), the panel records them as trusted copy, and the status row shows the tree error verbatim only when it is that hint; everything else gets the safe recovery message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): keep merge-gate diagnostics on the exact PR (#17248) * fix: keep merge-gate diagnostics on the exact PR * fix: print repair guidance for gate diagnostics * fix: allow merge-gate PR comments when permitted * fix: reject mismatched merge-gate event identities * fix: keep merge-gate alive on comment errors * fix: keep the cmux-cua credential out of the Codex argv (#17252) * test: codex wrapper must not put the cmux-cua credential in argv The Codex wrapper passes the cmux-cua socket credential as -c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any local user can read it with ps. These assertions require the value to be absent from argv, Codex to receive it in its environment, and the MCP server to receive it through env_vars. The fake Codex now builds the MCP server environment the way Codex does (allow-list + env_vars + env). * fix: keep the cmux-cua credential out of the Codex argv The wrapper passed the socket credential as a Codex -c env override, which put the value in the codex process argv where any local user can read it with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards the variable by name to the MCP server (Codex env_vars, openai/codex#5246). Codex's default shell_environment_policy excludes *TOKEN* names from agent shell commands. Attachment stays fail-closed when no credential resolves. * fix: keep codex wrapper overrides when the subcommand gets -c (#17257) * test: codex wrapper overrides must survive subcommand -c flags Codex declares -c/--config, --enable, and --disable as clap global arguments. When a subcommand such as exec or resume also receives one, Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's cmux-cua MCP config, hook config, and --disable computer_use placed before the subcommand are dropped. The fake Codex now applies that rule, and new cases cover exec, resume, mixed root and subcommand -c, and a literal prompt after --. * fix: keep codex wrapper overrides when the subcommand gets -c Codex declares -c/--config, --enable, and --disable as clap global arguments and keeps only the subcommand-level values when a subcommand also receives one. The wrapper put its cmux-cua MCP config, hook config, and --disable computer_use before the user's argv, so codex exec -c ... or codex resume ... -c ... dropped all of them. The wrapper now moves the user's subcommand-level global arguments, in order, in front of the subcommand. Codex reads one root-level list with the same precedence as before. Tokens after -- stay in place, and interactive launches without a subcommand are unchanged. * Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256) `cmux browser repl` is a persistent JavaScript REPL that agents use to drive cmux browser panes: Playwright page, locator, keyboard and mouse semantics with native trusted input, budgeted accessibility snapshots with diffs, tabs, cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and site tools. Guards live outside agent code: fill-only secrets with redaction and capture masking, a domain policy over every frame and fetch hop, a per-tab clipboard for session-created tabs, private per-session temp directories and bounded cells, fetches and timers. Agent work never moves the user's focus, hibernated tabs wake on use, and crashed tabs report how to recover. Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the CLA action cannot read more than 250 commits of one pull request). Same tree as that branch's head. * Cloud: VM file operation routes (port of #16936) (#17254) * Cloud: VM file operation routes (port of #16936), missing path answers 404 Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936 (feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat, write, mkdir, remove) with the Freestyle driver and gateway methods. Includes the fix from feat-cmux-next: Freestyle removes a missing path with success, so removeVmFile stats first and answers a missing file with 404 vm_file_not_found (any other stat failure, a missing VM included, stays a provider failure). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found The staging rehearsal of #17254 showed stat of a removed file answering 502 vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as remove already did, and title the error 'File not found'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop calling the legacy Subrouter during account deletion (#17273) * test: account deletion must not call the retired legacy Subrouter Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stop calling the legacy Subrouter during account deletion The legacy Subrouter at subrouter.cmux.dev is being retired. Account deletion now skips the legacy revoke phase and needs no legacy env vars. Local mapping rows are still deleted, and a legacy_delete_pending tombstone from an older deployment resumes at the hosted checkpoint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Split the account DELETE handler under the complexity limit DELETE delegates to deleteAccount and named phase helpers that share one progress record, so its complexity drops from 46 to under 20 and its grandfathered baseline entry is removed. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: remove merge gate and restore exact-head merging (#17275) * Cloud: private network routes (port of #16948) (#17255) * Cloud: private network routes (port of #16948), missing firewall rule answers 404 Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948 (feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete), /api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle driver, gateway and private-network workflow pieces. Includes the fix from feat-cmux-next: a firewall rule that is not in the caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race on delete too); a missing VM endpoint stays vm_not_found. Stacked on the file-routes port (#17254). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint No behavior change; makes the parser testable without the route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints The staging rehearsal of #17255 found two defects. validCidr compared the prefix with net.isIP(), which returns the family (4 or 6), so any IPv4 prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership check looked the VM up in the personal scope, but every new VM is team-owned, so vmId endpoints were vm_not_found; the route now resolves the account scope when a vmId is named, and the VM must be the caller's own (the firewall edits the caller's network). The provider now gets only the rule fields: the Freestyle driver spreads its input into the request body, so userId and provider were sent to Freestyle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): decide firewall rule ownership on the shared provider account The Freestyle account is shared by every cmux user, so the API decides whose a rule is. Reading and deleting: the rule names at least one resource and every resource it names is the caller's. get and delete now read the rule by id; they searched only the network listing, so a rule that named a VM and a CIDR was created (201) and then could not be found or deleted. The list merges the network listing with one listing per caller VM. Creating: the destination must be a caller resource (400 vm_invalid_firewall_rule), because a destination of only an address range or the public Internet would reach other tenants' machines. Every firewall call resolves the account scope like the other VM routes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs Security review P2: the provider adds selectors as new optional fields, and an unknown one could name another tenant's resource, so a rule with an unknown endpoint field is not the caller's. Review P3: send the canonical range so a valid non-canonical CIDR does not fail at the provider. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found' Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations These routes are new on a provider account shared by every cmux user. Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An unfiltered list reads the network plus at most the 10 newest live VMs, one provider call each; older VMs list with ?vmId. Create and delete are throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID (no other VM mutation route has a limiter, so this follows the team-invite limiter: fail closed when the firewall is unavailable, fail open and report when the rule is unset or removed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897) * remote-tmux: failing test for a torn-down stream feeding the next one A control stream torn down for a reconnect keeps delivering what its reader had already buffered. The test holds the main actor while a first client writes 560 KB, starts a reconnect, and expects none of those bytes to reach the connection. * remote-tmux: stop a torn-down stream from feeding the next one Cancelling the task that reads a control client's stdout does not empty the reader's buffer, so chunks the old client had already written were still ingested after the teardown. Once the reconnect had respawned, a leftover command result was taken for the new client's attach reply. The connection then never asked for windows and the mirror stayed blank for good. Each read loop now stops as soon as its process generation is no longer the current one, and closes its reader. --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> * remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237) * remote-tmux: failing test for a docked terminal closed when its window's mirrors move * remote-tmux: keep a window whose Dock has panels when its mirrors move out --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> * Expose the workspace task-status lane to custom sidebars (#17245) Custom sidebars could not read a workspace's task-status lane. cmux already resolves one per workspace and the control socket can pin it, but the interpreter data context carried no field for it, so a sidebar had no way to group or colour rows by whether a workspace needs attention. `workspaces[i].status` now carries the resolved lane as its raw wire value: todo, working, needs-attention, review or done. The snapshot takes it as a required parameter so a dropped wiring breaks the build rather than reporting a silent "todo". Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Clear restored agent notifications once the agent is gone (#17067) * test: prune read notifications of agents that died with the previous app An agent alive at quit dies without SessionEnd, so its last "Completed in" notification is restored on every launch and shown as the workspace's latest sidebar summary even though no agent is running. The stale-agent sweep must drop it once the pane has no agent again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: drop restored agent notifications once the agent does not return Notifications persist across relaunch so an unseen agent result is not lost, but an agent that was alive when cmux quit dies without SessionEnd. Nothing clears its notification afterwards: agent PIDs are not restored, so the 30s stale-PID sweep has no dead PID to catch. Restore now records the notifications of local panes that hosted an agent (resume binding or restorable agent snapshot). The stale-agent sweep removes the read ones when the pane has no agent PID again; unread ones survive until read, and a pane the agent resumes into is handed back to its hooks. Remote terminals are skipped since their agent can outlive the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: keep restored notifications while the resume is in flight Covers a read notification posted after restore (never tracked) and a pane whose restored resume has not reported an agent PID yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: defer restored notification prune while the resume is in flight The 30-second sweep can run before an auto-resumed agent reports its PID. Skip panes whose restored command is still in flight, using the coordinator's existing ownsInFlightRestoredCommand contract. Also look up tracked notifications by id instead of scanning the store per panel, and mark the value-only snapshot helper nonisolated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: keep read non-agent notifications on a restored agent pane The restore tracks every notification persisted on a pane that hosted an agent, so a read `cmux notify` banner on that pane is pruned with the agent's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: track only agent-produced notifications on restored agent panes Use the persisted `isAgentEvent` provenance instead of panel ownership so a `cmux notify` banner on a pane that hosted an agent is not retired with the agent's result. Unknown provenance restores as agent-produced, matching TerminalNotificationStore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Send sidebar links through the external-open rules (#7397) * browser: apply external-open rules to sidebar links The sidebar's pull-request and port links (SwiftUI and AppKit rows, and the open-all-pull-requests action) opened in the embedded browser whenever that preference was on, without consulting the URL rules that route a site to the system browser. Sites listed there cannot work in the embedded web view at all, so a rule now wins over the embedded preference on those paths, the same way it does for a click inside a page. * browser: require a user event before a link escapes to the system browser WebKit reports a script calling click() on an anchor as .linkActivated, the same as a real click, so the external-open rules on their own let a page hand itself a system-browser open at a moment of its choosing. The navigation-typed escape now also requires an AppKit event in flight (a key, left-mouse, or middle-mouse event) and never intercepts a download, on every path that consults the rules: the main navigation delegate, the target=_blank UI delegate, and both popup delegates. The context menu's Open Link in New Tab is a gesture by construction and says so. The event check is a bound rather than a proof: NSApp.currentEvent says an event is being dispatched, not that this navigation is the thing the user asked for. Middle-clicks arrive as otherMouse events and count. * browser: e2e coverage for external-open link routing BrowserExternalOpenRoutingUITests drives real WebKit link activations through the socket browser.click against a local fixture and asserts routing at the delegate layer, where popup-vs-link-activation behavior actually diverges and unit tests cannot reach. Escapes are captured to a file through the existing DEBUG-only UI-test sink (CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation handler's default opener, so CI never opens Safari. Four cases: a matched link click escapes while the embedded page stays put; an unmatched click navigates embedded; a scripted window.open to a matched host never escapes; a target=_blank form POST to a matched host stays embedded. The shared BrowserFixtureSocketTestCase gains subclass hooks for launch arguments and environment, falls back from the in-process socket client to nc -U and then the bundled cmux CLI, disables hidden-webview discarding for the backgrounded UI-test host, and polls browser.wait through the cold-start content-process transient. * browser: click links for real in the external-open UI tests A link now leaves for the system browser only while a real input event is in flight, and the socket browser.click runs JavaScript, so the matched and unmatched link cases click through accessibility the way a person does. The scripted popup and form cases keep the socket click, since a scripted action is what they test. * browser: keep only the sidebar links, drop the in-page activation change The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher. * browser: use a rule the pattern safety check accepts in the port-link test --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci: require merge checks only when their workflows exist (#17284) * test: cover merging repos without aggregate CI workflow * fix: make merge checks conditional on base workflows * ci: fall back to ancestor evidence for --main-fix (#17277) * ci: use nearest ancestor for main-fix evidence * ci: constrain ancestor evidence to path-filtered changes * ci: inspect renamed paths in ancestor evidence * ci: bound ancestor evidence traversal * ci: reject incomplete ancestor path comparisons * fix(session): sweep stale scrollback replay files (#16056) * test(session): cover replay sweep and permissions Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): sweep stale scrollback replay files Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): sweep stale replay files before restore Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): remove synchronous replay sweep from app init Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): gate restore on off-main replay cleanup Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(app): keep replay sweep off startup critical path Signed-off-by: Alejandro Florez <soyeladice@gmail.com> * fix(session): skip replay sweep under XCTest * fix(session): harden retained replay files during stale sweep * docs(session): keep crash-recovery gate semantics accurate * test(session): cover legacy replay permissions and sweep filters --------- Signed-off-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Cover dotted Claude project dir in session directory search (#4939) * docs: clarify Claude project dir decode asymmetry * test: cover dotted Claude project dir in session directory scope * test: scope Claude session roots per task instead of process env Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree. * test: cover Claude cwd-filter lookup without a DEBUG seam Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded. --------- Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Answer the tmux session commands Claude Code calls (#13632) * Answer the tmux session commands Claude Code calls Claude Code's tmux backend tears down and reattaches its agent panes with kill-session, switch-client, new-session -A and show-options -g prefix. The compatibility layer rejected all four, so a Teams session failed with "Unsupported tmux compatibility command" once it got past creating panes. A tmux session is a cmux workspace, which has-session and new-session already assume, so kill-session closes that workspace, switch-client selects it, and new-session -A attaches to it when it exists instead of creating a duplicate. show-options now answers from a table, and prefix reports the C-b that a default tmux client would. The sequence test drives all four through the real shim. Its fake socket also now unwraps the capability envelope that the shell integration adds inside a cmux terminal, so the test reports the behavior it checks rather than a JSON decode error; that unwrapping moved into the shared helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Reject a socket payload that is not a request The fake servers indexed request["method"] straight off json.loads, so a payload that decoded to null, a list, or an object with a non-string method raised inside the handler thread and surfaced as an unrelated CLI error. They now answer those with an error line, which names the real problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: reject empty fake socket request methods Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: reject a failed tmux session lookup and kill-session -a A workspace.list error must not become a new session, and kill-session -a must not close the caller. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep a failed tmux session lookup from creating a workspace new-session -A treated every resolution error as a missing session, and kill-session ignored -a and closed the caller. A missing session still creates; a lookup failure and an unsupported flag now fail first. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974) * Add failing test: sidebar nil-comparison yields nothing (#7943) In a custom sidebar, `x != nil` / `x == nil` against a bound optional field does not evaluate to true/false — it evaluates to nothing. Interpolation renders empty, ternaries always take the else branch, and `if x != nil` guards are never taken, so optional-guarded views never render. This commit adds only the regression test (no fix) so CI shows it red. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix sidebar nil-comparison to evaluate to a Bool (#7943) The interpreter's value model had no null case and no `nil`-literal evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the same value that means "expression unsupported / no value". `evalInfix` then bailed on the comparison, so `x != nil` / `x == nil` produced nothing: interpolation rendered empty, ternaries always took the else branch, and `if x != nil` guards were never taken. Optional-guarded views (including the shipped status-board.swift / finder.swift examples) silently drew nothing. - Add `SwiftValue.null` for the `nil` literal and for comparing an absent optional field against `nil` (distinct from host `nil` = "no value"). - Evaluate `NilLiteralExprSyntax` to `.null`. - Handle `==` / `!=` before the operand guards, coalescing an absent operand (host `nil`) and the `nil` literal to `.null`, so the comparison yields a Bool that is true/false when present and false/true when absent. Fixes #7943 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: preserve nil comparison evaluation failures * fix: preserve nested nil comparison misses * fix: preserve parenthesized nil comparison misses * fix: handle nil optional binding and equality budget --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> * fix: refresh merge helper and honor neutral checks (#17294) * test: cover neutral checks and helper checkout refresh * test: tolerate absent git diagnostics * fix: refresh clean main checkout before merging * test: cover cloud welcome close shortcut ownership * fix: route cloud welcome close shortcut to its window --------- Signed-off-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Leo <cheerleaderleo@outlook.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: BlueRaddish <jeeholife2@gmail.com> Co-authored-by: EJ <ej@campbell.name> Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: Philipp Mochine <philipp@mochine.de> Co-authored-by: mys <wowpotato@naver.com> Co-authored-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Sungho Park <relilau00@gmail.com> Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mark Xian <mark-xian@foxmail.com> Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com> --------- Signed-off-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Leo <cheerleaderleo@outlook.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: BlueRaddish <jeeholife2@gmail.com> Co-authored-by: EJ <ej@campbell.name> Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: Philipp Mochine <philipp@mochine.de> Co-authored-by: mys <wowpotato@naver.com> Co-authored-by: Alejandro Florez <soyeladice@gmail.com> Co-authored-by: Sungho Park <relilau00@gmail.com> Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mark Xian <mark-xian@foxmail.com>





Summary
Adds
cmux browser repl, a persistent JavaScript REPL that agents use to drive cmux browser panes. It has one Playwright-shaped API with native input, budgeted accessibility snapshots and site tools. Design:docs/browser-repl/README.md; driver contract and guards:docs/browser-repl/driver-protocol.md.page, locators, keyboard, mouse and events, plustabs,snapshot,screenshot, cookie-bearingfetch,fs/path/os,session,secretsandsites. Named sessions keep variables and tabs;cmux browser repl mcpserves a session over stdio MCP.interactiveandviewportmodes. Output is budgeted (20,000 characters printed, 25,000 per call, the rest spilled to a file).tabs.list()reportslive,hibernated,wakingorcrashed. A call on a hibernated tab wakes it off screen (bounded wait); a crashed tab fails with an error that says to callpage.reload().{ confirm: true }.Evidence
tests/browser-parity/gate.sh --app --app-runs 2on a fleet build of the final head (results in the merge receipt comment).docs/browser-repl/parity-report.md,docs/browser-repl/edge-cases.md). Excluded: raw CDP (WebKit has none).docs/browser-repl/performance.md).page.reload().Review notes
_frames:, resource-load delegate,_tryClose,_doAfterProcessingAllPendingMouseEvents:,_doAfterActivityStateUpdate:,_simulateMouseMove:,_WKContentWorldConfiguration.allowAccessToClosedShadowRoots, WebKit feature flags for the async Clipboard API.+[NSPasteboard pasteboardWithName:]for WebKit callers only, bounded to one command.browser.repl.*socket methods are not allowlisted for remote relay.gate.shon the fleet. CI adds only the CmuxBrowser package tests (about 15 s) and one app-host suite.Changelog
cmux browser repl, a persistent browser automation REPL for agents with native input, budgeted accessibility snapshots, guarded secrets and clipboard, and site tools.