Skip to content

feat(coworking): expose terminal scrollback to all four agents via opt-in MCP installer#948

Closed
chr1syy wants to merge 25 commits into
RunMaestro:rcfrom
chr1syy:feat/agentic-coworking
Closed

feat(coworking): expose terminal scrollback to all four agents via opt-in MCP installer#948
chr1syy wants to merge 25 commits into
RunMaestro:rcfrom
chr1syy:feat/agentic-coworking

Conversation

@chr1syy

@chr1syy chr1syy commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Coworking Encore Feature: agents can read terminal scrollback and inspect/drive in-app browser tabs on demand via a per-agent MCP server. Opt-in via Settings -> Encore Features -> Coworking -> Install for [agent].

  • One-shot installer writes a maestro-coworking MCP entry to each agent's user-level config (~/.claude.json, ~/.codex/config.toml, ~/.config/opencode/opencode.json, ~/.factory/mcp.json). Install once, persists. Node is resolved to an absolute path at install time (falls back to node on PATH) so GUI-launched agents under nvm/fnm/volta still find a runtime.
  • Each terminal tab gets a click-to-copy term:N pill so users can address them by name.
  • Terminal tools: list_terminals(), read_terminal(id, lines?).
  • Browser read tools: list_browsers(), get_browser_url(id), read_browser(id, format?, maxChars?) where format is text | innerText | html.
  • Browser interaction tools (opt-in per agent): browser_navigate(id, url), browser_back/forward/reload/stop(id), browser_click(id, selector), browser_type(id, selector, text), browser_eval(id, code), browser_screenshot(id).
  • Off by default. When the Encore flag is off the Settings panel and pills are hidden. Browser interaction is additionally gated behind a per-agent "Interaction" toggle (default off), enforced in the main-process bridge so a hand-configured MCP client cannot bypass the UI.

Architecture

  • Bundled MCP server - hand-rolled stdio server (no SDK runtime dep) written as <userData>/coworking-mcp-server.js on every app.ready. Stable absolute path survives Maestro upgrades. Advertises all tools; the agent's MCP client owns the subprocess lifecycle.
  • IPC bridge - Unix-domain-socket on POSIX, named pipe on Windows, at a stable per-userData path. Newline-delimited JSON-RPC. Per-connection sessionId binding (env + ppid fallback) so an agent only ever sees its own session.
  • Registry - main-process mirror of every session's terminal AND browser tabs, keyed by sessionId (no "active session" concept, for privacy). Browser tabs get a stable, monotonic, never-reused browser:N id assigned by the registry (no renderer-side schema change needed).
  • Browser op resolver - read text/HTML and all interaction need the LIVE webview, so they round-trip main -> renderer over a unique, sender-validated coworking:requestBrowserOp channel (8s timeout, under the MCP server's 10s per-RPC cap). list_browsers/get_browser_url are metadata-only and never touch a webview.

Browser resolution decision (activation vs background mount)

rc already keeps the active agent's browser tabs mounted (visible or hidden) via useBrowserTabMounting (the browserTabKeepAlive setting). We chose (A') prefer an already-mounted webview handle; activate the target tab only as a fallback:

  • The responder first looks up the target tab in the per-tab BrowserTabView handle map (lifted from MainPanelContent to MainPanel). If the tab is already mounted (even hidden via keep-alive), we use it directly with no focus change.
  • If it isn't mounted, we activate it (which mounts it), run the op, then restore the previously active browser tab.
  • The live webview only exists for the active agent. For a non-focused agent, an opt-in, LRU-capped background webview host mounts the tab off-screen so read/interaction still work without stealing focus; with the host disabled the responder returns a clear ok:false (metadata tools always work cross-session). See Hardening follow-ups.

Hidden multi-webview mounting for non-focused sessions (option B) is now available as an opt-in, capped background host (default off) - see Hardening follow-ups. The focused-agent already-mounted fast path remains the default and the only path when the host is off.

Files (high level)

  • src/shared/coworkingBrowser.ts - single source of truth for the browser op/result/input/entry contract (shared across main, preload, renderer).
  • src/main/coworking/* - registry (browser records + browser:N ids + interaction permission + agentType), tools (listBrowsers/getBrowserUrl/readBrowser/browserInteract + resolver), bridge dispatch + validateInteractionOp, server-script (browser tools in TOOLS + tools/call), audit log.
  • src/main/ipc/handlers/coworking.ts, src/main/preload/coworking.ts - browser sync + sender-validated browser-op round-trip.
  • src/renderer/... - useCoworkingRegistrySync mirrors browser tabs + per-agent interaction flag + hides flagged tabs; useCoworkingBrowserResponder resolves the live (or background) webview and runs ops; BrowserTabView gains extract/getMeta/navigate/reload/stop/executeJavaScript/capturePage + a hide-from-agent toggle; CoworkingSetup gets per-agent Interaction + confirm-policy controls and a global background-browsing toggle.
  • Settings: coworkingBrowserInteraction: string[], coworkingBrowserInteractionConfirm: Record<string, policy>, coworkingBackgroundBrowsers + ...Limit across settingsStore/defaults/settingsMetadata/useSettings.

Hardening follow-ups (this update)

Layered on top of the read + interaction tools above:

  • Per-call approval for state-changing ops. Before any browser_navigate/back/forward/reload/stop/click/type/eval/screenshot, the renderer can require explicit user approval via a cancel-safe confirm dialog. Policy is per-agent (off | dangerous | all, default dangerous = confirm navigate/eval), surfaced in the Coworking Setup panel. Interaction-op timeouts were raised (server bridge 330s / main resolver 300s) so a human has time to respond; reads stay at 8s.
  • Per-tab "hide from agent". A persisted hiddenFromAgent flag on BrowserTab with an eye toggle in the browser toolbar. Hidden tabs are filtered out of list_browsers / read_browser / interaction at the registry-sync boundary, so agents can neither see nor address them.
  • Browser-tool audit log. Every dispatched browser op is recorded (timestamp, sessionId, agentType, tool, op kind, redacted arg summary, outcome ok | error | denied) to the system log + a JSONL under userData. URLs/selectors are truncated and page content / eval code is never logged (lengths only).
  • Cross-session live access (opt-in). A capped, LRU-evicted background webview host (coworkingBackgroundBrowsers, default off; coworkingBackgroundBrowsersLimit default 2, clamped 1-10) mounts a non-focused agent's tab off-screen - its own partition preserved, isActive=false, aria-hidden, pointer-events off, no focus steal - so read + interaction work without switching the user's active agent. Disabling the setting releases every hidden webview. capturePage on a hidden webview returns a clear ok:false rather than a blank image.
  • Cleanup. Removed the unused @modelcontextprotocol/sdk dependency (see Notes).

New files: coworkingApprovalStore.ts, CoworkingApprovalHost.tsx, CoworkingBackgroundBrowsers.tsx, coworkingBackgroundBrowserStore.ts, coworking-audit.ts.

Notes

  • @modelcontextprotocol/sdk was declared but unused (the server is hand-rolled); it has been removed from package.json and the lockfile regenerated (npm install --package-lock-only).

Test plan

  • Typecheck (all three tsconfigs) + ESLint on changed files green.
  • Vitest: 126 coworking tests pass (registry incl. browser:N ids / scoping / interaction permission + hidden-tab filtering + agentType; tools + read truncation; bridge dispatch + interaction gate + op validation + audit; renderer responder op->handle mapping incl. the cross-session background path; per-call approval store + policy; background-host LRU / cap / clear).
  • End-to-end MCP smoke: drove the real generated server over stdio against a mock bridge - initialize / tools/list (all 12 tools) / tools/call for list_browsers, read_browser, plus an interaction op that proceeds when approved and surfaces a decline as an MCP error.
  • Manual in-app (requires a desktop build): toggle Coworking on, Install for an agent, open >=2 browser tabs, ask the agent to list_browsers / read_browser / (with Interaction on) browser_navigate + browser_click, confirm the approval dialog, and confirm the page changes.

Deferred (out of scope for this PR)

  • Subscriptions / push notifications, cross-tab references.
  • SSH-mode coworking (agent runs remote, terminals/browsers are local).
  • Inline directives fallback for any future non-MCP agent.

Summary by CodeRabbit

  • New Features
    • Added a Coworking settings panel to install/uninstall and check MCP setup for supported agents.
    • Introduced session-scoped coworking support for terminals and browsers, including optional browser interaction, background browsing, and confirmation prompts.
    • Added coworking “term:N” pills on terminal tabs with one-click copy.
    • Added browser tool audit logging and “hide/show from agents” controls.
  • Bug Fixes
    • Strengthened the coworking bridge handshake and RPC validation to prevent cross-session data leakage.
    • Improved stable coworking ID assignment and terminal ordering, including legacy session compatibility and safer buffer scoping/truncation.

…t-in MCP installer

Adds a Settings → Encore Features → Coworking panel that installs a Maestro-managed
MCP server entry into each supported agent's user-level config (Claude Code,
Codex, OpenCode, Factory Droid). When installed, the agent gains two read-only
tools — list_terminals and read_terminal — that surface scrollback from the
currently active AI tab's terminal tabs. Each terminal renders a click-to-copy
"term:N" pill so users can address them by name in chat.

Architecture:
- Stable bundled MCP stdio server written to <userData>/coworking-mcp-server.js
  on every app ready. Spawned by the agent's own MCP client (per protocol);
  Maestro just keeps a Unix-domain-socket / Windows named-pipe IPC bridge alive.
- Per-agent installer strategies under src/main/coworking/installers/ —
  comment-preserving JSON merge for Claude Code / OpenCode / Factory Droid,
  sentinel-delimited block append for Codex's TOML.
- Renderer pushes active-session terminal records to a main-process registry;
  the registry feeds list_terminals and resolves "term:N" → tabUuid; main asks
  renderer for buffers via a request-response IPC channel.
- Encore flag `coworking` (off by default) gates the Settings panel and the
  pill UI. Bridge runs whenever Maestro is up; per-agent activation is opt-in.

Tests: 36 new coworking tests + extended terminalTabHelpers tests cover registry
filtering, tool truncation, per-agent install/uninstall round-trips with comment
preservation, and monotonic non-reusing coworkingId minting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 21c2d971-d37d-4069-97d7-4bbc5ba26a93

📥 Commits

Reviewing files that changed from the base of the PR and between 74e322f and ca5bb60.

📒 Files selected for processing (1)
  • src/__tests__/renderer/hooks/coworking/coworkingApproval.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/renderer/hooks/coworking/coworkingApproval.test.ts

📝 Walkthrough

Walkthrough

Adds coworking support across shared contracts, session-scoped registry and tools, bridge/server routing, per-agent installers, IPC/preload wiring, and renderer state and UI for terminal and browser access.

Changes

Coworking MCP Terminal and Browser Access

Layer / File(s) Summary
Shared contracts and settings
src/main/coworking/coworking-types.ts, src/main/coworking/coworking-session-id.ts, src/main/coworking/pid-resolution.ts, src/shared/coworkingBrowser.ts, src/shared/settingsMetadata.ts, src/main/stores/defaults.ts, package.json, src/__tests__/main/coworking/coworking-types.test.ts, src/__tests__/main/coworking/coworking-session-id.test.ts, src/__tests__/main/coworking/pid-resolution.test.ts
Defines coworking IDs, terminal and bridge types, browser operation and confirmation contracts, session-id and PID resolution helpers, settings metadata and defaults, dependency metadata, and related type tests.
Renderer state, settings, and UI
src/renderer/types/index.ts, src/renderer/utils/terminalTabHelpers.ts, src/renderer/stores/coworkingApprovalStore.ts, src/renderer/stores/coworkingBackgroundBrowserStore.ts, src/renderer/stores/settingsStore.ts, src/renderer/hooks/settings/useSettings.ts, src/renderer/components/Settings/searchableSettings.ts, src/renderer/components/MainPanel/BrowserTabView.tsx, src/renderer/components/MainPanel/MainPanel.tsx, src/renderer/components/coworking/*, src/renderer/components/Settings/CoworkingSetup.tsx, src/renderer/components/TabBar/TerminalTabItem.tsx, src/renderer/hooks/coworking/*, src/renderer/global.d.ts, src/__tests__/renderer/utils/terminalTabHelpers.test.ts, src/__tests__/renderer/hooks/coworking/*, src/__tests__/renderer/stores/coworkingBackgroundBrowserStore.test.ts
Adds stable coworking id minting, approval queueing, background browser mounts, coworking settings persistence, browser extraction and visibility controls, renderer API typings, searchable settings, UI wiring, and supporting renderer tests.
Main-process registry and coworking tools
src/main/coworking/coworking-registry.ts, src/main/coworking/coworking-tools.ts, src/__tests__/main/coworking/coworking-registry.test.ts, src/__tests__/main/coworking/coworking-tools.test.ts, src/__tests__/main/coworking/coworking-browser.test.ts
Reworks the coworking registry around session-scoped terminal and browser state, and adds terminal/browser lookup and scrollback tool helpers with matching tests.
Bridge server and bundled MCP script
src/main/coworking/coworking-audit.ts, src/main/coworking/coworking-bridge.ts, src/main/coworking/coworking-server-paths.ts, src/main/coworking/coworking-server-script.ts, src/__tests__/main/coworking/coworking-bridge.test.ts, src/__tests__/main/coworking/coworking-browser-bridge.test.ts
Adds the bridge socket server, session handshake, browser auditing, bundled MCP stdio script, bridge RPC client, and server-path helpers.
Per-agent installers and orchestration
src/main/coworking/installers/types.ts, src/main/coworking/installers/claude-code.ts, src/main/coworking/installers/codex.ts, src/main/coworking/installers/factory-droid.ts, src/main/coworking/installers/opencode.ts, src/main/coworking/installers/index.ts, src/main/coworking/coworking-installer.ts, src/__tests__/main/coworking/installers/*
Defines the installer contract, implements config-file installers for supported agents, registers them, and adds status/install/uninstall orchestration with tests.
IPC handlers, preload API, and app lifecycle
src/main/ipc/handlers/coworking.ts, src/main/ipc/handlers/index.ts, src/main/preload/coworking.ts, src/main/preload/index.ts, src/main/index.ts
Registers coworking IPC handlers, exposes maestro.coworking in preload, and wires coworking startup/shutdown into the Electron lifecycle.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • RunMaestro/Maestro#613: Both changes touch src/renderer/components/TabBar/TerminalTabItem.tsx, which now adds coworking id display and clipboard behavior on top of the tab UI.
  • RunMaestro/Maestro#710: Both changes touch src/renderer/components/MainPanel/MainPanel.tsx, where this PR adds coworking sync and responder wiring into the panel lifecycle.
  • RunMaestro/Maestro#785: Both changes affect the browser tab view and handle surface that the coworking browser responder builds on.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: opt-in coworking MCP support for terminal scrollback across the four agents.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented May 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a Coworking feature that lets each AI agent read its own session's terminal scrollback on demand via a per-agent MCP server. The architecture is well-designed: a bundled stdio MCP server in userData, a Unix-socket/named-pipe IPC bridge for secure main-process round-trips, and per-agent one-shot installers that write user-level config files with comment-preserving round-trips.

  • Session isolation is enforced at the bridge handshake (hello binds each MCP connection to its caller's MAESTRO_COWORKING_SESSION_ID or falls back to PID-tree walking for agents like Codex that don't propagate env). The fail-closed design from the privacy fix in feat(coworking): expose terminal scrollback to all four agents via opt-in MCP installer #948 is preserved throughout.
  • Four installers (Claude Code, Codex, OpenCode, Factory Droid) use comment-json for JSON configs and a sentinel-delimited text block for Codex's TOML, keeping unrelated user content byte-exact on both install and uninstall.
  • PID-walk fallback (pid-resolution.ts) uses synchronous execFileSync on macOS/Windows from within the Electron main process \u2014 the main issue requiring attention before merge.

Confidence Score: 4/5

Safe to merge on Linux; the Codex PID-walk path blocks the Electron main process synchronously on macOS and Windows and should be made async before shipping to those platforms.

The session isolation design is solid and the privacy regression from #948 is not reintroduced. The one concrete defect on the changed path is in pid-resolution.ts: execFileSync is called synchronously inside the main-process bridge handler for every Codex MCP handshake that falls through to PID resolution. On macOS this can stall the event loop for hundreds of milliseconds across multiple hops; on Windows, wmic cold-start makes this worse.

src/main/coworking/pid-resolution.ts — getParentPid needs to be converted to async (execFile + promisify) and threaded through resolveSessionFromPidWalk and CoworkingSessionFromPidResolver before the Codex path is safe to ship on macOS and Windows.

Important Files Changed

Filename Overview
src/main/coworking/pid-resolution.ts PID-walk fallback for Codex session binding — uses execFileSync synchronously on macOS/Windows, blocking the Electron main process event loop for each hop during handshake
src/main/coworking/coworking-bridge.ts Unix-socket/named-pipe IPC bridge with session-scoped handshake; logic is sound and fail-closed on missing session binding
src/main/coworking/coworking-server-script.ts Self-contained MCP stdio server bundled to userData; bridgeCall correctly captures local socket reference; once('error', reject) is not removed after connect
src/main/ipc/handlers/coworking.ts Buffer-request resolver uses per-request unique channel, validates sender ID, and correctly cleans up listener on both success and timeout paths
src/main/coworking/coworking-registry.ts Session-keyed terminal registry with clean per-session sync/remove semantics; correctly avoids the active-session singleton that caused the privacy bug
src/renderer/hooks/coworking/useCoworkingRegistrySync.ts Mirrors all session terminals to main-process registry; correctly tears down on feature-flag disable
src/renderer/hooks/coworking/useCoworkingBufferResponder.ts Answers main-process buffer-fetch requests from per-session TerminalView refs; does not gate on the coworking Encore flag
src/main/coworking/installers/codex.ts Sentinel-delimited TOML block approach avoids a TOML parser dependency; install/uninstall/replace all handled correctly
src/main/coworking/installers/claude-code.ts Uses comment-json for round-trip-safe JSON; idempotent install/uninstall with correct hasOwnProperty check
src/main/process-manager/ProcessManager.ts Injects MAESTRO_COWORKING_SESSION_ID env at spawn time for non-terminal processes; correctly strips composite AI-tab suffix
src/main/coworking/coworking-tools.ts Clean tool implementations with injected resolver for testability; readTerminal correctly trims trailing newline before line counting
src/main/coworking/coworking-server-paths.ts Resolves Node binary path at install time via which/where; caches result; falls back to 'node' on failure

Sequence Diagram

sequenceDiagram
    participant Agent as Agent CLI (e.g. Codex)
    participant MCP as coworking-mcp-server.js (stdio)
    participant Bridge as Coworking Bridge (main process)
    participant Registry as CoworkingRegistry (main)
    participant Renderer as Renderer

    MCP->>Bridge: connect (Unix socket / named pipe)
    MCP->>Bridge: "hello { sessionId | ppid }"
    Bridge-->>MCP: "{ ok: true }"
    MCP->>Bridge: "listTerminals {}"
    Bridge->>Registry: listForSession(sessionId)
    Registry-->>Bridge: "[{ id, cwd, title }]"
    Bridge-->>MCP: result
    MCP->>Bridge: "readTerminal { id, lines? }"
    Bridge->>Registry: resolveTabUuidForSession(sessionId, id)
    Registry-->>Bridge: tabUuid
    Bridge->>Renderer: webContents.send(requestBuffer, tabUuid, sessionId, responseChannel)
    Renderer-->>Bridge: ipcRenderer.send(responseChannel, content)
    Bridge-->>MCP: "result { content, truncated, totalLines }"
Loading

Reviews (3): Last reviewed commit: "Merge branch 'rc' into feat/agentic-cowo..." | Re-trigger Greptile

Comment thread src/renderer/hooks/coworking/useCoworkingRegistrySync.ts Outdated
Comment thread src/main/ipc/handlers/coworking.ts Outdated
Comment thread src/main/coworking/coworking-server-script.ts
Comment thread src/main/ipc/handlers/coworking.ts Outdated
Comment thread src/main/coworking/coworking-server-paths.ts Outdated
@pedramamini

Copy link
Copy Markdown
Collaborator

Hey @chr1syy — thanks so much for putting this together! The Coworking design (one-shot user-level MCP install + term:N pills + scoped read-only tools) is a really nice shape, and I appreciate the deferred-scope breakdown.

Heads up: the branch currently has merge conflicts against rc (mergeStateStatus: DIRTY). Could you kindly rebase onto the latest rc and resolve the conflicts so we can get a clean merge? Once that's green I'll do a full review pass.

Thanks again!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (1)
src/main/coworking/coworking-types.ts (1)

79-88: ⚡ Quick win

Adopt these ID helpers everywhere term:N is built/parsing manually.

Now that formatCoworkingId / parseCoworkingId exist, replace hardcoded term: string math in coworking registry/renderer call sites to prevent format drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/coworking/coworking-types.ts` around lines 79 - 88, Several call
sites build or parse the public coworking id via string math; replace those with
the new helpers to avoid format drift by using formatCoworkingId(...) when
constructing IDs and parseCoworkingId(...) when reading them. Search for manual
"term:" concatenations and Number/id slicing in the coworking registry and
renderer code, update construction sites to call formatCoworkingId(coworkingId)
and replace parsing sites to call parseCoworkingId(id) and handle the null case
appropriately (e.g., treat as invalid input). Ensure all references to the
literal "term:" are removed so formatCoworkingId and parseCoworkingId are the
single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/coworking/coworking-registry.ts`:
- Around line 110-116: In notify(), do not leave the catch block empty: when
iterating this.listeners in notify() catch exceptions thrown by fn() and report
them using the Sentry utilities (import captureException and/or captureMessage
from src/utils/sentry.ts) with contextual info (e.g., which listener or a
descriptive message referencing notify/listeners). Preserve isolation by not
letting one listener stop others (continue iteration after reporting), and for
truly unexpected, non-recoverable errors re-throw after capturing so global
error handling (Sentry) can record them.

In `@src/main/coworking/coworking-server-paths.ts`:
- Around line 27-29: The current use of fs.promises.readFile(scriptPath,
'utf8').catch(() => null) swallows all I/O errors; change it to only suppress
ENOENT and rethrow other errors so unexpected failures surface. Locate the read
and comparison around scriptPath and contents and replace the blanket catch with
logic that, on read failure, returns null only if err.code === 'ENOENT' and
otherwise throws the error (either by using an explicit try/catch around the
await or a .catch(err => { if (err.code === 'ENOENT') return null; throw err;
})). Ensure behavior otherwise remains: if existing === contents return
scriptPath.

In `@src/main/coworking/coworking-server-script.ts`:
- Around line 70-115: connectBridge and bridgeCall currently allow concurrent
callers to create multiple sockets that share the module-level bridgeBuffer and
leave pending RPCs unresolved on clean close; fix by serializing connection
attempts and rejecting in-flight RPCs on disconnect. Implement a single shared
"connectingPromise" returned by connectBridge when a connection is in-flight so
concurrent bridgeCall() callers await the same socket; only set bridgeConn after
the socket's 'connect' callback succeeds and initialize a fresh per-connection
bridgeBuffer there; on 'error' and 'close' handlers reject all entries in
pending with a clear Error (e.g., "bridge disconnected") and pending.clear(),
set bridgeConn = null and connectingPromise = null; ensure connectBridge clears
connectingPromise on both success and failure so subsequent attempts can retry.
Reference symbols: connectBridge, bridgeCall, bridgeConn, bridgeBuffer, pending,
connectingPromise.

In `@src/main/coworking/coworking-tools.ts`:
- Around line 45-59: The logic in the tailing/counting block uses
full.split('\n') directly, which treats a trailing newline as an extra empty
line; update the handling in the resolver result path (the code around
resolver(tabUuid), the args.lines branch and the totalLines calculation) to
normalize by trimming a single trailing '\n' before splitting (i.e., if full
endsWith '\n' remove that final newline only), then compute allLines, totalLines
and the slice(-args.lines) from that normalized string so totalLines and
truncated/tail results reflect real lines; apply the same normalization to the
final totalLines calculation (where totalLines = full.length === 0 ? 0 :
full.split('\n').length).

In `@src/main/index.ts`:
- Around line 1034-1040: The try/catch around the coworking bridge bootstrap
(wrapping ensureCoworkingServerScript and startCoworkingBridge) currently only
logs a warning and swallows the error; update the catch to call the Sentry
reporting utility (captureException) with the caught error and contextual
message before logging/continuing so failures are reported to production error
tracking; reference the existing functions ensureCoworkingServerScript and
startCoworkingBridge and use captureException (from the project's sentry
utilities) to capture the exception and include a short context string like
"Failed to start coworking bridge" when calling captureException.

In `@src/main/ipc/handlers/coworking.ts`:
- Around line 133-142: The IPC response handler currently accepts responses from
any renderer via ipcMain.once(responseChannel) which allows spoofing; update the
listener used in the Promise to validate the sender by checking that
_event.sender.id === win.webContents.id (or equivalently comparing _event.sender
=== win.webContents) and if it doesn't match, ignore/reject that specific call
(e.g., call reject or simply return and keep waiting until the timeout) so only
the expected BrowserWindow response resolves; adjust the
ipcMain.once(responseChannel, (_event, content) => { ... }) callback to perform
this sender validation and ensure the timeout/caller cleanup (clearTimeout,
remove listeners) still happens correctly using responseChannel and
BUFFER_REQUEST_TIMEOUT_MS.

In `@src/renderer/components/Settings/CoworkingSetup.tsx`:
- Around line 55-76: The per-agent handlers (e.g., handleInstall) only set
busyAgentId which disables a single row but leaves the bulk action
(installAll/uninstallAll) active; update the UI and handlers so the bulk button
is disabled whenever busyAgentId is non-null and add a guard at the start of
bulk handlers (and at the start of per-agent handlers) to return early if
busyAgentId is set; specifically, use the existing busyAgentId state and
setBusyAgentId to block overlapping operations, update the bulk action
components to check busyAgentId !== null to disable the button, and add an
early-return guard in handleInstall/handleUninstall and the
installAll/uninstallAll handlers to prevent concurrent mutations and races
before calling refresh().

In `@src/renderer/components/TabBar/TerminalTabItem.tsx`:
- Around line 111-118: The click handler handleCoworkingPillClick should await
navigator.clipboard.writeText inside a try/catch instead of calling .then(); on
success call flashCopiedToClipboard(), and on failure call captureException (or
captureMessage) from src/utils/sentry.ts with contextual info (include
coworkingPillId and the operation name) and consider optional user-visible
fallback; update handleCoworkingPillClick to e.stopPropagation(), check
coworkingPillId, then try { await
navigator.clipboard.writeText(coworkingPillId); flashCopiedToClipboard(); }
catch (err) { captureException(err, { extra: { coworkingPillId, action:
'copyCoworkingPill' } }); }.

In `@src/renderer/hooks/coworking/useCoworkingBufferResponder.ts`:
- Around line 15-35: The current onRequestBuffer handler swallows all exceptions
in a broad try/catch causing terminal read failures to disappear; replace the
blanket try/catch around getTerminalBuffer lookups with explicit handling: only
treat missing buffers as recoverable (resulting in empty content), but for
unexpected exceptions call the Sentry helper (captureException or captureMessage
from src/utils/sentry.ts) with context (tabUuid, sessionId, handler id) and
re-throw the error so it surfaces to global error tracking; keep the normal flow
that calls window.maestro.coworking.sendBufferResponse(responseChannel, content)
for successful or legitimately-empty reads, but do not convert unexpected
exceptions into silent empty strings.

In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts`:
- Around line 24-61: The hook currently swallows all errors in the three catch
blocks around window.maestro.coworking.setActiveSession and
syncSessionTerminals; change each catch to inspect the thrown error and only
silently ignore known teardown/IPC-not-ready errors (e.g., "main process not
ready", "IPC channel closed" or whatever specific error.message/name your IPC
emits), but for any other/unexpected errors call captureException(error) (or
captureMessage for string errors) and rethrow the error so failures are
observable; apply this change to the setActiveSession calls in useEffect
teardown and the early-return branch and to the try/catch that calls
setActiveSession + syncSessionTerminals (refer to
window.maestro.coworking.setActiveSession and
window.maestro.coworking.syncSessionTerminals and lastPayloadRef) so only
expected teardown IPC failures are suppressed and all other errors are reported
and propagated.

In `@src/renderer/utils/terminalTabHelpers.ts`:
- Around line 122-126: The code currently defaults nextCoworkingId to 1
(nextCoworkingId = session.nextCoworkingId ?? 1) which can collide with existing
terminalTabs for legacy sessions; instead compute the nextCoworkingId when
session.nextCoworkingId is undefined by scanning session.terminalTabs (or the
terminalTabs array in scope) for existing numeric coworkingId suffixes (e.g.,
term:N), take the max and add 1, then assign coworkingId: tab.coworkingId ??
computedNextCoworkingId when building tabWithCoworkingId; apply the same
scan-and-compute fix to the other occurrence around the code that sets
nextCoworkingId at the second location (the block near the other occurrence you
noted).

---

Nitpick comments:
In `@src/main/coworking/coworking-types.ts`:
- Around line 79-88: Several call sites build or parse the public coworking id
via string math; replace those with the new helpers to avoid format drift by
using formatCoworkingId(...) when constructing IDs and parseCoworkingId(...)
when reading them. Search for manual "term:" concatenations and Number/id
slicing in the coworking registry and renderer code, update construction sites
to call formatCoworkingId(coworkingId) and replace parsing sites to call
parseCoworkingId(id) and handle the null case appropriately (e.g., treat as
invalid input). Ensure all references to the literal "term:" are removed so
formatCoworkingId and parseCoworkingId are the single source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3d817612-f8fb-49f2-b1c1-65dedc3259e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1338860 and 14653e9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (38)
  • package.json
  • src/__tests__/main/coworking/coworking-registry.test.ts
  • src/__tests__/main/coworking/coworking-tools.test.ts
  • src/__tests__/main/coworking/coworking-types.test.ts
  • src/__tests__/main/coworking/installers/claude-code.test.ts
  • src/__tests__/main/coworking/installers/codex.test.ts
  • src/__tests__/main/coworking/installers/factory-droid.test.ts
  • src/__tests__/main/coworking/installers/opencode.test.ts
  • src/__tests__/renderer/utils/terminalTabHelpers.test.ts
  • src/main/coworking/coworking-bridge.ts
  • src/main/coworking/coworking-installer.ts
  • src/main/coworking/coworking-registry.ts
  • src/main/coworking/coworking-server-paths.ts
  • src/main/coworking/coworking-server-script.ts
  • src/main/coworking/coworking-tools.ts
  • src/main/coworking/coworking-types.ts
  • src/main/coworking/installers/claude-code.ts
  • src/main/coworking/installers/codex.ts
  • src/main/coworking/installers/factory-droid.ts
  • src/main/coworking/installers/index.ts
  • src/main/coworking/installers/opencode.ts
  • src/main/coworking/installers/types.ts
  • src/main/index.ts
  • src/main/ipc/handlers/coworking.ts
  • src/main/ipc/handlers/index.ts
  • src/main/preload/coworking.ts
  • src/main/preload/index.ts
  • src/renderer/components/MainPanel/MainPanel.tsx
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/components/Settings/searchableSettings.ts
  • src/renderer/components/Settings/tabs/EncoreTab.tsx
  • src/renderer/components/TabBar/TerminalTabItem.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/coworking/useCoworkingBufferResponder.ts
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/terminalTabHelpers.ts
  • src/shared/settingsMetadata.ts

Comment thread src/main/coworking/coworking-registry.ts
Comment thread src/main/coworking/coworking-server-paths.ts
Comment thread src/main/coworking/coworking-server-script.ts
Comment thread src/main/coworking/coworking-tools.ts Outdated
Comment thread src/main/index.ts
Comment thread src/renderer/components/Settings/CoworkingSetup.tsx Outdated
Comment thread src/renderer/components/TabBar/TerminalTabItem.tsx
Comment thread src/renderer/hooks/coworking/useCoworkingBufferResponder.ts
Comment thread src/renderer/hooks/coworking/useCoworkingRegistrySync.ts Outdated
Comment thread src/renderer/utils/terminalTabHelpers.ts Outdated
- Untrack package-lock.json and add to .gitignore.
- Fix display-name index bug in useCoworkingRegistrySync — getTerminalTabDisplayName
  now receives the original terminalTabs index, not the post-filter index, so
  pre-feature tabs with no coworkingId no longer skew the agent-visible "Terminal N"
  fallback names (greptile P1).
- Fix coworkingId reuse on legacy sessions: addTerminalTab now falls back to
  max(existing) + 1 when nextCoworkingId is missing, and bumps strictly past the
  larger of the chosen id and the bumped counter (coderabbit major).
- Trailing-newline off-by-one in readTerminal: a buffer ending with \n no
  longer adds a synthetic empty line to totalLines/tail truncation
  (coderabbit major).
- Coworking IPC buffer-request handler: read sessionId once at dispatch (drop
  pointless IIFE; greptile P1), use scoped removeListener instead of
  removeAllListeners (greptile P2), validate sender id against the main window
  to reject spoofed responses (coderabbit major).
- coworking-server-paths: only suppress ENOENT on the read-existing path
  (coderabbit major); add resolveNodeCommand() that resolves an absolute node
  path via which/where at install time, falling back to literal "node" if
  resolution fails — addresses GUI-launched-agent PATH issues with nvm/fnm/volta
  (greptile P2). buildMcpServerSpec is now async to thread the resolved node.
- coworking-server-script: serialize concurrent connectBridge() calls behind a
  shared promise, reset the per-connection bridgeBuffer on connect, reject all
  pending RPCs on both error and clean close, and capture the resolved socket
  locally in bridgeCall to plug the null-deref + pending-leak hole
  (greptile P2 + coderabbit major).
- Sentry capture for previously-swallowed errors: CoworkingRegistry.notify(),
  app-startup bridge bootstrap, useCoworkingBufferResponder catch, and the
  TerminalTabItem clipboard pill click. useCoworkingRegistrySync distinguishes
  expected teardown IPC errors (silently skipped) from unexpected ones
  (captured) per project Sentry guidelines (coderabbit major x5).
- CoworkingSetup: bulk install/uninstall and per-agent install/uninstall now
  short-circuit when any other mutation is in flight, and the bulk button
  disables on anyBusy to prevent concurrent writes to the same user-level
  config file (coderabbit major).
- New regression tests: trailing-newline behavior in readTerminal, empty-buffer
  case, and legacy-session coworkingId fallback in addTerminalTab. Scoped test
  count: 92 (was 89).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/renderer/hooks/coworking/useCoworkingRegistrySync.ts (1)

27-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unexpected IPC failures are still swallowed by reportIfUnexpected().

This helper captures and returns, so the .catch(...) paths below still turn broken coworking sync into a no-op. That leaves the main registry stale until another effect happens. Re-throw after reporting, or only catch the known teardown cases at the call sites. As per coding guidelines, "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production."

Suggested patch
-function reportIfUnexpected(err: unknown, scope: string): void {
+function reportIfUnexpected(err: unknown, scope: string): never | void {
 	if (isExpectedTeardownIpcError(err)) return;
 	void captureException(err instanceof Error ? err : new Error(String(err)), {
 		extra: { scope: `useCoworkingRegistrySync:${scope}` },
 	});
+	throw err instanceof Error ? err : new Error(String(err));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts` around lines 27 -
32, The helper reportIfUnexpected currently captures unexpected IPC errors then
returns, which swallows them; update reportIfUnexpected (and its uses) so that
after calling captureException (using captureException(err instanceof Error ?
err : new Error(String(err)), { extra: { scope:
`useCoworkingRegistrySync:${scope}` } })), the function re-throws the error
unless isExpectedTeardownIpcError(err) returned true; this ensures unexpected
errors bubble up instead of turning coworking sync into a no-op. Use the same
error object (or a new Error(String(err)) when needed) when re-throwing to
preserve stack/context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/coworking/coworking-server-script.ts`:
- Around line 188-194: The initialize handler currently hardcodes
protocolVersion to '2025-06-18' in the rpcResult payload; change it to negotiate
using the client's params.protocolVersion (from the incoming params) and your
server-supported versions: if params.protocolVersion is in your supported list
accept and echo it, otherwise select a compatible supported version (e.g.,
highest-preferred supported version or a documented fallback) and use that value
in the rpcResult sent by send(); update the code around the initialize branch
that builds the rpcResult so it computes negotiatedProtocolVersion and assigns
that instead of the literal '2025-06-18' (refer to the initialize handling
logic, rpcResult call, and send invocation).

In `@src/main/index.ts`:
- Around line 852-855: The shutdown call to stopCoworkingBridge() is currently
swallowing all errors; modify the promise catch to report failures to Sentry
instead of silently ignoring them: import and call the Sentry utility
(captureException or captureMessage) from src/utils/sentry.ts inside the catch
handler for stopCoworkingBridge(), include contextual info (e.g., "quit-time
stopCoworkingBridge failure") and still keep it best-effort (do not rethrow), so
shutdown errors are recorded in Sentry while app quit continues.

In `@src/main/ipc/handlers/coworking.ts`:
- Around line 131-145: Wrap the call to
win.webContents.send('coworking:requestBuffer', tabUuid, sessionId,
responseChannel) in a try/catch; if send() throws, immediately remove the
temporary IPC listener (ipcMain.removeListener(responseChannel, handler),
referencing responseChannel and handler), clear the timeout (clearTimeout(timer)
referencing timer), and reject the Promise with the caught error (or a new Error
with context) so the promise does not hang until BUFFER_REQUEST_TIMEOUT_MS
elapses. Ensure the same cleanup path is used as the timeout branch to avoid
leaking the listener.

In `@src/renderer/utils/terminalTabHelpers.ts`:
- Around line 125-144: The code uses session.nextCoworkingId as-is which can be
lower than existing coworkingIds; compute the starting id by clamping against
maxExistingCoworkingId + 1 (e.g. chosenNext = Math.max(session.nextCoworkingId
?? 0, maxExistingCoworkingId + 1)), use that chosenNext when assigning
tabWithCoworkingId.coworkingId (if tab.coworkingId is undefined), and then bump
and store nextCoworkingId as Math.max(chosenNext + 1,
(tabWithCoworkingId.coworkingId ?? 0) + 1) so we never reuse an existing id;
adjust references to nextCoworkingId, maxExistingCoworkingId,
tabWithCoworkingId, and the final nextCoworkingId assignment accordingly.

---

Duplicate comments:
In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts`:
- Around line 27-32: The helper reportIfUnexpected currently captures unexpected
IPC errors then returns, which swallows them; update reportIfUnexpected (and its
uses) so that after calling captureException (using captureException(err
instanceof Error ? err : new Error(String(err)), { extra: { scope:
`useCoworkingRegistrySync:${scope}` } })), the function re-throws the error
unless isExpectedTeardownIpcError(err) returned true; this ensures unexpected
errors bubble up instead of turning coworking sync into a no-op. Use the same
error object (or a new Error(String(err)) when needed) when re-throwing to
preserve stack/context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6495532-261a-482a-bc6d-229457fd4e84

📥 Commits

Reviewing files that changed from the base of the PR and between 14653e9 and c3a9892.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • .gitignore
  • src/__tests__/main/coworking/coworking-tools.test.ts
  • src/__tests__/renderer/utils/terminalTabHelpers.test.ts
  • src/main/coworking/coworking-installer.ts
  • src/main/coworking/coworking-registry.ts
  • src/main/coworking/coworking-server-paths.ts
  • src/main/coworking/coworking-server-script.ts
  • src/main/coworking/coworking-tools.ts
  • src/main/index.ts
  • src/main/ipc/handlers/coworking.ts
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/components/TabBar/TerminalTabItem.tsx
  • src/renderer/hooks/coworking/useCoworkingBufferResponder.ts
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
  • src/renderer/utils/terminalTabHelpers.ts
✅ Files skipped from review due to trivial changes (3)
  • .gitignore
  • src/main/coworking/coworking-registry.ts
  • src/tests/renderer/utils/terminalTabHelpers.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/coworking/coworking-installer.ts
  • src/main/coworking/coworking-tools.ts
  • src/tests/main/coworking/coworking-tools.test.ts

Comment thread src/main/coworking/coworking-server-script.ts
Comment thread src/main/index.ts
Comment thread src/main/ipc/handlers/coworking.ts Outdated
Comment thread src/renderer/utils/terminalTabHelpers.ts
…#948 review round 2

Merge resolves a single modify/delete conflict in package-lock.json (kept
deleted, now gitignored on this branch). All other conflicts auto-merged
cleanly: package.json, src/main/index.ts (new cue-telemetry + cue-stats
imports), src/main/preload/index.ts (new createCueStatsApi), src/main/ipc/
handlers/index.ts (new registerCueStatsHandlers), src/renderer/components/
Settings/searchableSettings.ts, src/renderer/global.d.ts, src/renderer/
types/index.ts, and src/shared/settingsMetadata.ts.

Round 2 review feedback:

- coworking-server-script.ts: implement MCP protocolVersion negotiation in the
  initialize handler. Echo the client's protocolVersion if it's in our
  SUPPORTED_PROTOCOL_VERSIONS list (newest-first), otherwise fall back to the
  newest supported. Previously hardcoded to 2025-06-18, which violates the
  MCP spec for clients that enforce strict version matching (coderabbit major).
- index.ts: capture exceptions on quit-time stopCoworkingBridge() failures via
  Sentry instead of silently swallowing them. Stale socket / pipe on next
  launch is now observable in production (coderabbit major).
- ipc/handlers/coworking.ts: wrap webContents.send() in try/catch. If send
  throws (e.g. window destroyed between the guard and the call), reject the
  promise immediately and remove the temporary IPC listener instead of leaking
  it for the full 5s timeout window (coderabbit major).
- renderer/utils/terminalTabHelpers.ts: clamp nextCoworkingId with
  Math.max(persisted ?? 1, maxExistingCoworkingId + 1) instead of just falling
  back when undefined. Protects against persisted-counter-lower-than-existing-id
  drift (corrupted save / older buggy build), which would otherwise hand out a
  duplicate term:N (coderabbit major).
- New regression test: stale-counter clamp scenario (coworkingId=10 + counter=3
  → next add must pick 11, not 3). Scoped test count: 93 (was 92).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/coworking/coworking-server-script.ts`:
- Around line 138-156: bridgeCall() can hang forever if the bridge stops
responding; add a per-RPC timeout so pending entries are cleaned and the
returned promise rejects after a deadline. When creating the pending entry in
bridgeCall (use the same identifiers: bridgeCall, pending, nextRpcId,
connectBridge, conn.write), start a timer (e.g., 30s) that deletes
pending.get(id) and rejects the promise with a timeout Error; store the timer
handle alongside resolve/reject in pending so the response handler can clear it;
when conn.write throws or when resolve/reject is called for a real response,
clear the timer and remove the pending entry to avoid leaks. Ensure the timeout
is rejected with a clear message like "coworking bridge: rpc timeout" so callers
can handle it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3a0d43f-cf3a-4d2c-b628-5084ad5144d9

📥 Commits

Reviewing files that changed from the base of the PR and between c3a9892 and 0f79261.

📒 Files selected for processing (12)
  • package.json
  • src/__tests__/renderer/utils/terminalTabHelpers.test.ts
  • src/main/coworking/coworking-server-script.ts
  • src/main/index.ts
  • src/main/ipc/handlers/coworking.ts
  • src/main/ipc/handlers/index.ts
  • src/main/preload/index.ts
  • src/renderer/components/Settings/searchableSettings.ts
  • src/renderer/global.d.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/terminalTabHelpers.ts
  • src/shared/settingsMetadata.ts
✅ Files skipped from review due to trivial changes (2)
  • src/shared/settingsMetadata.ts
  • src/renderer/components/Settings/searchableSettings.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/preload/index.ts
  • src/main/ipc/handlers/coworking.ts
  • src/renderer/utils/terminalTabHelpers.ts
  • src/tests/renderer/utils/terminalTabHelpers.test.ts
  • src/main/ipc/handlers/index.ts

Comment thread src/main/coworking/coworking-server-script.ts Outdated
chr1syy and others added 2 commits May 3, 2026 16:59
CI failure root cause: actions/setup-node@v6 with cache:npm requires a
lockfile, and untracking package-lock.json broke both lint-and-format and
test jobs at the npm-cache-setup step. Upstream/rc tracks the lockfile (the
merge attempted to restore it). Reverting the .gitignore entry from c3a9892
and re-adding package-lock.json — was the wrong call to untrack it.

Round 3 review feedback (coderabbit major):
- coworking-server-script.ts: add a per-RPC timeout (BRIDGE_RPC_TIMEOUT_MS =
  10s) inside bridgeCall(). Without this, a wedged-but-not-disconnected
  bridge can stall tools/call indefinitely because pending entries never
  resolve. The timer expiry deletes the pending entry first (so a late reply
  is a no-op) and rejects with a clear error; both resolve and reject are
  wrapped to clearTimeout on the success path; the conn.write try/catch also
  clears the timer when the write itself throws. 10s comfortably covers the
  main-process buffer-fetch cap (5s) plus jitter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…namespace

CI test job was failing with TypeError: Cannot read properties of undefined
(reading 'setActiveSession') across MainPanel tests. Root cause: those tests
mock window.maestro without including the new `coworking` namespace, so the
hooks bound at MainPanel mount crashed at the first useEffect tick.

Defensive guard pattern (matches what existing hooks already do in this
codebase): each hook reads `window.maestro?.coworking` once and bails cleanly
when undefined. This also covers older preload bundles where the namespace
isn't exposed yet (the feature degrades to off rather than crashing).

- useCoworkingRegistrySync: early-return when bridge is missing; use the
  resolved `bridge` reference for all subsequent IPC calls.
- useCoworkingBufferResponder: early-return + use resolved `bridge` for
  onRequestBuffer subscription and sendBufferResponse reply.
- CoworkingSetup.refresh(): when bridge is missing, set statuses to [] and
  finish loading instead of throwing into notifyToast.

Local repro fixed: vitest src/__tests__/renderer/components/MainPanel +
src/__tests__/main/coworking + src/__tests__/renderer/utils/terminalTabHelpers
all pass (281 tests, was failing 131+ MainPanel tests in CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts`:
- Around line 22-25: The current isExpectedTeardownIpcError function is too
permissive because its regex includes "cannot be cloned", which hides real
serialization/structured-clone bugs; update isExpectedTeardownIpcError to only
treat true teardown messages ("No handler registered" and "Object has been
destroyed") as expected by removing the "cannot be cloned" branch from the regex
(keep the function name and return behavior otherwise) so serialization errors
are allowed to surface to Sentry for debugging.
- Around line 78-90: The code updates lastPayloadRef.current before the async
IPC completes, so failures in bridge.setActiveSession or
bridge.syncSessionTerminals leave lastPayloadRef stuck and prevent retries;
change the flow so the ref is only updated after the awaited calls succeed (or,
if you must keep the optimistic set, revert lastPayloadRef.current =
null/previousPayload inside the catch that calls reportIfUnexpected) so that
failed syncs allow the next effect run to retry; adjust the async IIFE around
bridge.setActiveSession and bridge.syncSessionTerminals and reference
lastPayloadRef, payload, and reportIfUnexpected accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 738fc16e-c148-4862-9987-2349a68defc7

📥 Commits

Reviewing files that changed from the base of the PR and between f35651e and 721f036.

📒 Files selected for processing (3)
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/hooks/coworking/useCoworkingBufferResponder.ts
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/hooks/coworking/useCoworkingBufferResponder.ts

Comment thread src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
Comment thread src/renderer/hooks/coworking/useCoworkingRegistrySync.ts Outdated
- Drop "cannot be cloned" from the expected-teardown regex in
  isExpectedTeardownIpcError. That message is a structured-clone bug
  (caller put a non-serializable value in the IPC payload) — a real
  programming error, not a teardown condition. Letting Sentry see it
  is the point (coderabbit minor).
- Roll back lastPayloadRef.current = '' inside the sync catch so the
  next effect run retries the failed sync. Previously the ref was set
  optimistically before the async IIFE resolved, so any IPC failure
  would leave the main-process registry stale forever — until the
  user switched sessions or toggled the flag — because the cache-hit
  short-circuit would block all subsequent attempts (coderabbit major).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Blocker: list_terminals / read_terminal scope is bound to the UI's focused AI tab, not the caller agent

Found while soaking #948 into 0.16.0-RC-build for a 0.16.17-RC test build. Install round-trip and end-to-end tool execution work cleanly; this finding is about the scoping model of the MCP tools.

Repro

  1. Coworking flag on, install for claude-code (session A in this repro).
  2. Open a terminal in session A → pill term:3 (after some open/close churn).
  3. Open a terminal in session B (a different agent in the Left Bar).
  4. From within claude-code in session A, run a tool with a delay so the user can switch UI focus during the call window:
    sleep 8 && date
  5. During the sleep, click session B's AI tab in the Left Bar so it becomes the focused tab.
  6. After the sleep, call mcp__maestro-coworking__list_terminals from claude-code (still session A — its agent process didn't move).

Observed: list_terminals returns {id: "term:1", title: "Terminal 1"} — that's session B's terminal, not session A's term:3.
Expected: session A's terminal(s) only — claude-code in session A should never see session B's terminals regardless of UI focus.

Why this is a privacy issue

The PR description says "Scoped to the active AI tab's session only." In practice that means scoped to whichever AI tab the user is currently focused on, not to the calling agent. With Maestro's whole point being long-running agents that work in the background, this is exploitable:

  • Agent A keeps running while the user switches focus to session B (very common workflow).
  • Agent A calls list_terminals / read_terminal during that window.
  • Agent A silently reads scrollback from session B — different agent, possibly different project, possibly secrets in the buffer.
  • No UI signal that this is happening; nothing in the buffer indicates it was read by a foreign agent.

Architectural cause (from PR description)

Renderer ↔ main — renderer pushes active-session terminal records via `coworking:syncSessionTerminals`; main asks renderer for buffers via webContents.send → responseChannel pattern.

The registry stores a singleton "current" snapshot that gets overwritten on every focus change. Any agent reading the registry reads whatever was last pushed. There's no per-session keying.

Suggested fix

Scope must be caller-bound, not focus-bound:

  1. Bind MCP server to its session at install time. Each agent's maestro-coworking MCP entry should carry its session ID (env var or arg), so the bridge can identify the caller.
  2. Per-session registry, keyed by session ID. coworking:syncSessionTerminals should push for one session at a time without clobbering others (e.g., Map<sessionId, TerminalRecord[]>).
  3. list_terminals / read_terminal lookups use the caller's session ID from the bridge handshake to read only that session's slice of the registry.
  4. Optional follow-up: a UI signal (toast / pill flash) when an agent reads a terminal, so the user is aware of the read regardless of focus state.

Other findings from the same soak (non-blocking, FYI)

  • ✅ Default-off behavior correct.
  • ✅ Install diff for claude-code is minimal-additive — only the mcpServers.maestro-coworking block added, plus the syntactically-required trailing comma. Zero collateral edits.
  • ✅ MCP server starts and tools function end-to-end (read_terminal returned accurate scrollback).
  • ✅ Stable IDs — closing term:1 did not recycle the ID for newly opened terminals.
  • ⚠️ list_terminals returns cwd: \"\" (empty string) — schema description promises a cwd value.
  • ⚠️ UX papercut: tab display title renumbers positionally ("Terminal 1") while the pill exposes the stable monotonic handle (term:3); they can diverge and confuse users when an agent references a term:N that doesn't match the visible tab number.

@chr1syy chr1syy added the blocking Can't move forward due to some hurdle. label May 4, 2026
Closes the privacy blocker reported on PR RunMaestro#948: list_terminals and
read_terminal returned whichever Maestro session the user was focused on,
so an agent running in the background could read another agent's
terminals when focus shifted mid-call.

Approach:
- Inject MAESTRO_COWORKING_SESSION_ID at agent CLI spawn (ProcessManager)
  so every MCP subprocess inherits its caller's session id.
- MCP server reads the env var on startup; hard-fails if missing rather
  than degrading to a permissive default.
- Bridge requires a `hello` handshake naming the session id before any
  tool call; per-connection sessionId is stored in a WeakMap and used
  to scope dispatch.
- Registry drops the `activeSessionId` singleton and the corresponding
  `setActiveSession` / `listForActiveSession` / `resolveTabUuidForActiveSession`
  helpers; lookups now take an explicit sessionId.
- Renderer pushes terminals for every session (not just the focused one)
  so agents in the background still see their own terminals.
- cwd field falls back to session.cwd / projectRoot so the schema's
  cwd promise is no longer violated with an empty string.
- Added handshake/binding tests; updated registry + tools tests for the
  per-session API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai re-review please
@coderabbitai re-review please

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@chr1syy Sure, triggering a fresh full review of PR #948 now.

I also want to highlight that the scoping issue you documented is a significant finding. The core problem — the CoworkingRegistry maintaining a singleton active-session pointer that gets overwritten on every UI focus change — means any agent can silently read another session's terminals during a focus switch. Your suggested fix (session-keyed registry + caller-bound session ID baked into the MCP server spec at install time) is the right architectural direction. A few additional notes on the suggested approach:

  1. Session ID in the MCP env: The MAESTRO_COWORKING_SESSION env var (or similar) should be written into the agent's config at install time and passed through the bridge handshake, so the bridge can verify the caller's session ID without trusting client-supplied params.
  2. Per-session registry slices: coworkingRegistry already stores per-session data internally (the Map<sessionId, ...> structure exists per coworking-registry.ts), but setActiveSession + listForActiveSession collapse it to a single active pointer. Changing dispatch() in coworking-bridge.ts to use the session ID from the connection context instead of the registry's active-session pointer would be the minimal fix.
  3. The two non-blocking findings (cwd: "" and pill vs. display-name divergence) are worth addressing before the feature ships, but don't block the architecture fix.

[full_review]

@chr1syy

chr1syy commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Regression — list_terminals / read_terminal now return nothing for the caller's own session

Re-tested PR #948 against today's build (d035932f fix(coworking): bind MCP tools to caller session, not UI focus). The privacy bug from yesterday's report is plugged — but the fix over-corrected: an agent can no longer read its own session's terminals.

Repro

  1. Settings → Encore Features → Coworking on, install for claude-code. (~/.claude.json install diff is still clean-additive — that part is fine.)
  2. Open one or more terminal tabs in claude-code's session — verify they exist visually with term:N pills.
  3. From within claude-code, call mcp__maestro-coworking__list_terminals.

Observed:

{ "terminals": [] }

read_terminal({ id: "term:N" }) for any visible N returns:

MCP error -32000: coworking tools: terminal 'term:N' not found in your session (it may have been closed)

Expected: the caller's own visible terminals listed.

Tried to rule out a sync race

  • Re-probed list_terminals multiple times — consistently empty.
  • Asked the user to refocus a terminal tab (would trigger any focus-change push) — still empty.
  • Asked the user to close all terminals and open a single fresh one (would trigger the add path) — still empty.
  • The error string "not found in your session" confirms the bridge is correctly identifying the caller's session — it just has nothing in the registry under that key.

Diagnosis

The fix changed the read side (caller-bound lookup) but appears to have left the write side incompletely wired. Two candidate causes:

  1. Session-id mismatch between install-time capture and live agent. The MCP server's ~/.claude.json entry has no per-session identifier (just the shared socket). If the bridge identifies callers by something captured at install time but the running claude-code agent has a different session ID (e.g., after an app restart between install and use), the lookup will always miss. Possible if the claude-code subprocess inherits a session id that no longer matches what the renderer pushes under.
  2. Per-session push not firing for the caller's session. coworking:syncSessionTerminals may only fire under specific lifecycle triggers (focus change, etc.) and/or push under a key that doesn't match what the read side looks up.

A quick way to disambiguate would be a log line in the bridge for both sides — "received sync push for session=X with N terminals" and "list_terminals lookup for session=Y" — to see whether X and Y match.

Severity

Blocker. The feature is now non-functional for its primary use case. Yesterday's behavior at least let the agent read its own session's terminals (the bug was the focus-driven cross-session leak); today's behavior reads nothing. The new tool description ("You will never see terminals belonging to other Maestro sessions, regardless of which tab the user has focused") accurately describes what's happening, but the inverse — "you WILL see your own session's terminals" — isn't holding.

Side note from soak testing — term:N ID reuse

User feedback during this re-test: closing terminals leaves their term:N slots unused; the next-opened terminal jumps to a higher N (we got to term:7 after a series of opens/closes). The display title ("Terminal 1" — positional, since only one is open) and the pill (term:7) diverge by 6.

This was a non-blocking nit in yesterday's report. Two reasonable directions:

  • Reuse closed IDs — fixes the divergence, but breaks any agent-side cache of a previously-read term:N (the same id now points to a different terminal). Recommend against unless agents are explicitly told to never cache.
  • Align display title to the stable pill ID — show the tab as "Terminal N" where N is the pill number. Eliminates divergence without breaking the stability contract.

Probably worth deciding direction now while the registry is being reworked anyway. Not part of this blocker, just rolling it forward.

chr1syy and others added 5 commits May 7, 2026 22:44
…composite

Follow-up to d035932. The privacy fix injected
`MAESTRO_COWORKING_SESSION_ID = config.sessionId` at agent spawn, but
ProcessManager spawns AI tabs with a composite id of the form
`{maestroSessionId}-ai-{tabId}` (and `-fp-{ts}` for forced-parallel).
The renderer pushes terminal records keyed by the bare `Session.id`,
so the bridge handshake bound the connection to a key that never had
records — `list_terminals` came back empty for the caller's own session.

Adds `resolveOwningMaestroSessionId(spawnSessionId)` in
src/main/coworking/coworking-session-id.ts, which strips the
`-ai-{tabId}[-fp-{ts}]` suffix (and the legacy `-ai`) so the env var
matches what the renderer pushes under. Non-AI spawn flavors
(synopsis-, batch-, group-chat-…) pass through unchanged — they have
no terminals to read, so the registry returning [] for them is fine.

Tests cover bare-id passthrough, AI-tab unwrap, fp suffix unwrap,
UUID-shaped session ids, the legacy `-ai` form, and confirmation that
`-ai` inside a hyphenated word does not trigger the parser.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ismatch

Adds three log lines to triage the in-app "list_terminals returns
not-accessible" report on PR RunMaestro#948:

- IPC sync: prints which sessionId the renderer is pushing under and
  how many records it's pushing.
- Bridge hello: prints the sessionId the MCP subprocess announces.
- Bridge listTerminals: prints the bound sessionId, every sessionId
  the registry currently knows about, and the size of the result.
- MCP server stderr: prints SESSION_ID + SOCKET on startup so we can
  confirm the env var actually reached the subprocess.

Plus a `knownSessionIds()` helper on CoworkingRegistry so the bridge
log can read live state without poking private fields.

Will be reverted once the self-session list_terminals path is verified
end-to-end against a real claude-code agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four diagnostic log points added in d9d4727 (bridge hello,
bridge listTerminals, IPC syncSessionTerminals, MCP server stderr
startup) and the supporting registry.knownSessionIds() helper are
removed. They served their purpose for triage; the branch now ships
clean for the next production build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…andshake

Codex CLI only passes env declared in its config TOML to MCP subprocesses,
so MAESTRO_COWORKING_SESSION_ID never reached coworking-mcp-server.js and the
server hard-failed before initialize. Adds a parent-PID fallback: hello sends
ppid alongside the optional sessionId, the bridge walks the process tree (cap
5 hops) to find a tracked agent-CLI PID via ProcessManager.getSessionIdByPid,
and rejects when neither source resolves — preserving the fail-closed privacy
property from PR RunMaestro#948. Claude Code/OpenCode keep using the fast env path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflicts with upstream additions that landed independently:
- package.json: keep both new dependencies (@lezer/highlight from rc +
  @modelcontextprotocol/sdk from this branch) and take rc's @sentry/electron
  bump.
- package-lock.json: regenerated against the merged package.json.
- src/renderer/types/index.ts: TerminalTab gains both coworkingId (this
  branch) and startupCommand / startupCommandCwd (rc) — orthogonal fields.
- src/renderer/components/Settings/tabs/EncoreTab.tsx: keep both imports
  (CoworkingSetup from this branch + Cue settings/service from rc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@chr1syy

chr1syy commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment on lines +37 to +65
if (isMacOS()) {
const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
encoding: 'utf8',
timeout: 1000,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
if (!out) return null;
const n = Number(out);
return Number.isInteger(n) && n > 0 ? n : null;
}
if (isWindows()) {
// wmic is deprecated but still present on Windows 10/11. PowerShell would
// be more future-proof but costs ~200ms cold-start per invocation, which
// is far too slow for a handshake-critical path.
const out = execFileSync(
'wmic',
['process', 'where', `ProcessId=${pid}`, 'get', 'ParentProcessId', '/value'],
{
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
}
);
const m = out.match(/ParentProcessId=(\d+)/);
if (!m) return null;
const n = Number(m[1]);
return Number.isInteger(n) && n > 0 ? n : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 execFileSync blocks the Electron main process event loop

getParentPid uses execFileSync synchronously on macOS and Windows. Since resolveSessionFromPid is invoked inside dispatch() without await, each call holds the main process event loop hostage for the duration of ps (macOS) or wmic (Windows). With MAX_PID_WALK_HOPS = 5, the worst case is 5 consecutive execFileSync forks before failing — that's potentially 250 ms on macOS and well over 1 s on Windows. During that window every IPC channel is stalled.

The fix is to make getParentPid async (using execFile + promisify), make resolveSessionFromPidWalk async, thread await through dispatch, and type CoworkingSessionFromPidResolver as (pid: number) => Promise<string | null>. Linux is already safe since /proc reads complete in < 1 ms.

Surface unexpected IPC failures to global error tracking instead of
silently swallowing them (sync path rolls back its optimistic cache
first so the next effect run retries). Disable-clear stays best-effort
to preserve loop isolation. Addresses PR RunMaestro#948 review feedback.
…read_browser)

Mirror per-session browser-tab metadata into the coworking registry (registry-
assigned stable browser:N ids) and add three read tools end to end: bridge
methods, MCP server-script tools, sessionId-scoped tool fns, and a sender-
validated main->renderer browser-op resolver. read_browser drives the live
webview via a new BrowserTabView extract()/getMeta() handle, preferring an
already-mounted (possibly hidden) tab and activating+restoring as a fallback
(decision A'); list/get_url are metadata-only. Browser op contract lives in
src/shared/coworkingBrowser.ts as the single source of truth.
Add state-changing browser tools (navigate/back/forward/reload/stop/click/
type/eval/screenshot) routed through a single gated browserInteract bridge
method. Permission is a per-agent-type setting (coworkingBrowserInteraction)
mirrored per-session into the registry and enforced in the bridge (hand-
configured MCP clients cannot bypass the UI toggle). BrowserTabView gains
navigate/reload/stop/executeJavaScript/capturePage handle primitives; the
responder composes click/type/eval via executeJavaScript with JSON-escaped
args and returns screenshots as MCP image content. CoworkingSetup gets a
per-agent Interaction toggle (default off).
… coverage

22 new main-process tests (registry browser:N id assignment/scoping/permission,
tools format+truncation, bridge dispatch + interaction permission gate +
validateInteractionOp) and 8 renderer tests for the responder op->handle mapping
(read/navigate/back/forward/reload/stop/click/type/eval/screenshot). Export
applyBrowserOp as a test seam. 96 coworking tests pass.
Export resolveAndRun as a test seam and add coverage for the renderer
resolution policy (decision A'): active-session guard returns ok:false for a
non-focused agent, the mounted-handle fast path avoids stealing focus, and the
activate-then-restore fallback mounts an unmounted tab and restores the prior
active tab. 100 coworking tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/__tests__/main/coworking/coworking-browser.test.ts (1)

184-209: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a tool-level cross-session browserInteract test.

This suite covers cross-session rejection for readBrowser, but browserInteract still only checks the happy path and an unknown id. Because the bridge suite mocks browserInteract, a regression in the real session-scoped tabUuid lookup would slip through here even though this boundary is privacy-sensitive.

Possible addition
+	it('browserInteract rejects cross-session access', async () => {
+		await expect(
+			browserInteract(
+				's2',
+				{ id: 'browser:1', op: { kind: 'reload' } },
+				{
+					registry,
+					resolver: async (): Promise<BrowserOpResult> => ({ ok: true }),
+				}
+			)
+		).rejects.toThrow(/not found in your session/);
+	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/main/coworking/coworking-browser.test.ts` around lines 184 -
209, Add a tool-level cross-session browserInteract test to cover the real
session-scoped tabUuid lookup path, not just the happy path and unknown id
cases. Extend the existing browserInteract tests to call it with a browser id
owned by a different session and assert it rejects, using browserInteract and
the shared registry/resolver setup to verify the boundary behavior. This should
mirror the cross-session coverage already present for readBrowser so a
regression in tab resolution is caught even when the bridge suite mocks
browserInteract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/__tests__/main/coworking/coworking-browser.test.ts`:
- Around line 184-209: Add a tool-level cross-session browserInteract test to
cover the real session-scoped tabUuid lookup path, not just the happy path and
unknown id cases. Extend the existing browserInteract tests to call it with a
browser id owned by a different session and assert it rejects, using
browserInteract and the shared registry/resolver setup to verify the boundary
behavior. This should mirror the cross-session coverage already present for
readBrowser so a regression in tab resolution is caught even when the bridge
suite mocks browserInteract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02cd0985-e51d-4fd7-9a6b-df28dac95a5e

📥 Commits

Reviewing files that changed from the base of the PR and between 8648f20 and d3c279d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • package.json
  • src/__tests__/main/coworking/coworking-browser-bridge.test.ts
  • src/__tests__/main/coworking/coworking-browser.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

State-changing browser ops now route through a renderer-owned approval queue
(coworkingApprovalStore + CoworkingApprovalHost) before execution: the responder
awaits requestApproval and returns ok:false on decline. Cancel/close resolves
false (cancel-safe, no hang). Per-agent policy coworkingBrowserInteractionConfirm
(off | dangerous | all; default dangerous = confirm navigate + eval) configurable
in CoworkingSetup. Interaction-op timeouts raised to 5min (bridge + main resolver)
so a human has time to approve; reads stay 8s. 110 coworking tests pass.
Hide-from-agent: BrowserTab.hiddenFromAgent (persisted) + a per-tab eye toggle in
the browser header; useCoworkingRegistrySync filters hidden tabs so list/read/
interaction never see them.

Audit: every browser tool call is recorded (ok | error | denied) with agentType
(mirrored per-session into the registry), redacted detail (urls/selectors
truncated, eval code/typed text reduced to lengths, never page content), via a
sink (system log + JSONL under userData). 92 main coworking tests pass.
…apped)

Lets a Coworking agent read/drive its own browser tabs while the user is
focused on a DIFFERENT agent, without stealing focus.

- coworkingBackgroundBrowserStore: hidden-mount registry + handles, LRU cap
  (1-10), clear() on opt-out.
- CoworkingBackgroundBrowsers host: renders requested tabs off-screen
  (fixed/aria-hidden/pointer-events:none, isActive=false); onUpdateTab routes
  to the OWNING session via normalizeBrowserTabUpdates.
- resolveAndRun: non-focused requests resolve a background handle; approval
  uses the REQUESTING agent's policy; blank capturePage on a hidden webview
  returns ok:false instead of an empty image.
- Settings: coworkingBackgroundBrowsers (default off) + ...Limit (default 2,
  clamped 1-10) across store/defaults/metadata/useSettings; global toggle +
  limit control in CoworkingSetup. Disabling clears live mounts.
- Mounted in MainPanel. Tests: 6 store (dedup/LRU/cap/clear) + cross-session
  read/interact + blank-screenshot responder tests. 126 coworking tests pass.
The coworking MCP server is hand-rolled (no SDK import anywhere in src).
Dropped from package.json and regenerated package-lock.json via
`npm install --package-lock-only --ignore-scripts` (no native build), which
also pruned its orphaned ajv/json-schema transitive subtree.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/components/MainPanel/MainPanel.tsx (1)

231-239: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make browser fallback selection session-aware.

Line 232 receives sessionId but Line 235 routes through active-session-scoped onBrowserTabSelect. If the caller’s tab is not already mounted, a non-focused agent can fail to read/drive its own browser because fallback selection targets the focused session. Route this through a session-scoped selection path, or require/ensure the background mount exists before resolving.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/MainPanel/MainPanel.tsx` around lines 231 - 239,
Browser fallback selection in selectBrowserTab is still routed through the
active-session-scoped onBrowserTabSelect, so non-focused agents can’t recover
their own tab when it isn’t mounted. Update MainPanel’s
selectBrowserTab/useCoworkingBrowserResponder flow to use the sessionId argument
and route selection through a session-scoped browser selection path, or ensure
the background mount exists before calling the fallback; keep the fix centered
on selectBrowserTab and onBrowserTabSelect.
src/renderer/hooks/coworking/useCoworkingRegistrySync.ts (1)

156-159: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Roll back lastSessionIdsRef when sync fails.

Line 159 commits currentIds before the IPC batch succeeds. If a removed session’s removeSession fails, the catch only clears lastPayloadRef, so the next run no longer includes that session in removed and the main registry can keep stale tabs. Update the session-id cache only after success, or restore the previous set in the catch.

🐛 Proposed fix
 		const payload = JSON.stringify({ perSession, perSessionBrowsers, removed });
 		if (payload === lastPayloadRef.current) return;
+		const previousSessionIds = lastSessionIdsRef.current;
 		lastPayloadRef.current = payload;
-		lastSessionIdsRef.current = currentIds;

 		(async () => {
 			try {
 				for (const sid of removed) {
 					await bridge.removeSession(sid);
@@
 				for (const { sessionId, inputs, interactionEnabled, agentType } of perSessionBrowsers) {
 					await bridge.syncSessionBrowsers(sessionId, inputs, interactionEnabled, agentType);
 				}
+				lastSessionIdsRef.current = currentIds;
 			} catch (err) {
 				lastPayloadRef.current = '';
+				lastSessionIdsRef.current = previousSessionIds;
 				reportIfUnexpected(err, 'sync');
 			}
 		})();

Also applies to: 172-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts` around lines 156 -
159, In useCoworkingRegistrySync, the session-id cache is being updated before
the IPC sync batch has actually succeeded, so a failed removeSession path can
leave stale tabs unaccounted for on the next retry. Move the
lastSessionIdsRef.current update so it only happens after the successful batch
completion, or save and restore the previous session-id set in the catch block
when the sync fails. Keep lastPayloadRef and the session-id cache in sync with
the actual outcome of the IPC operations.
🧹 Nitpick comments (2)
src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts (1)

198-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: approval is prompted even when the op can't run (non-focused tab, background browsing off).

For an interaction op targeting a non-focused tab while coworkingBackgroundBrowsers is disabled, the user is asked to approve, and the op then fails with the "not live" message. Consider resolving liveness/host availability before prompting so users aren't shown an approval dialog for an action that cannot execute.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts` around lines
198 - 263, The approval prompt is happening before checking whether the target
browser tab is actually runnable, so non-focused ops can ask for consent and
then fail when background browsing is off. Update useCoworkingBrowserResponder
to resolve tab liveness/host availability first using the existing isFocused and
resolveBackgroundHandle flow, and only call requestApproval for state-changing
ops after confirming the op can execute. Keep the early return path for the
existing “Browser tab is not live” failure so users are not prompted for
impossible actions.
src/renderer/components/coworking/CoworkingBackgroundBrowsers.tsx (1)

67-68: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Memoize the per-key ref callback

The responder only reads background handles via async polling in defaultResolveBackgroundHandle, so the brief null from ref detach/reattach isn’t observable there. The inline callback still triggers two setHandle writes and extra Map copies on every re-render; memoizing the ref callback per m.key avoids that churn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/coworking/CoworkingBackgroundBrowsers.tsx` around
lines 67 - 68, The inline ref callback on BrowserTabView is recreated on every
render, causing extra setHandle writes and Map churn even though
defaultResolveBackgroundHandle only polls asynchronously. Memoize the per-key
callback used in CoworkingBackgroundBrowsers so the same ref function is reused
for each m.key, and keep the existing setHandle behavior unchanged while
removing the unnecessary reattachment churn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/coworking/coworking-audit.ts`:
- Around line 52-55: The `redactBrowserOpDetail` helper is still logging
`navigate` URLs with query strings and fragments, which can leak tokens/PII into
the audit output and JSONL sink. Update the `navigate` branch to sanitize
`op.url` before truncating it: strip any search and hash components, then keep
the existing 200-character limit. Make the same redaction behavior apply
wherever `BrowserOp` details are serialized for the audit log so
`coworking-browser-audit.jsonl` never receives the full URL.
- Around line 77-79: The audit write in coworking-audit.ts is using a
synchronous main-thread disk call that can block Electron responsiveness. Update
the audit path in the audit entry handling logic to use asynchronous file
writing instead of appendFileSync, ideally by making the audit helper async and
awaiting the write so browser tool events do not stall the main process. Keep
the same file target and JSONL format, but ensure the write path in the audit
function is non-blocking and that any callers of the coworking audit entry flow
handle the async result appropriately.

In `@src/main/coworking/coworking-bridge.ts`:
- Around line 416-424: The audit detail for browser navigation is still exposing
sensitive URL parts, so update redactBrowserOpDetail(op) to fully scrub
browser_navigate payloads before auditedBrowserCall logs them. Ensure the URL is
normalized to origin/path only, or explicitly remove search and hash from the op
detail before it reaches the audit entry. Keep the change localized around
redactBrowserOpDetail and the browser_interact/auditedBrowserCall flow so all
navigation audits use the sanitized form.

In `@src/renderer/components/MainPanel/MainPanel.tsx`:
- Around line 1097-1098: The approval host is currently mounted only inside the
normal session view, so requests can’t surface a modal when MainPanel takes an
early-return branch like Log Viewer, Agent Sessions, Memory Viewer, or the empty
state. Move CoworkingApprovalHost out of the conditional render path in
MainPanel so it mounts in a parent scope that always survives those branches,
while keeping CoworkingBackgroundBrowsers where appropriate.

---

Outside diff comments:
In `@src/renderer/components/MainPanel/MainPanel.tsx`:
- Around line 231-239: Browser fallback selection in selectBrowserTab is still
routed through the active-session-scoped onBrowserTabSelect, so non-focused
agents can’t recover their own tab when it isn’t mounted. Update MainPanel’s
selectBrowserTab/useCoworkingBrowserResponder flow to use the sessionId argument
and route selection through a session-scoped browser selection path, or ensure
the background mount exists before calling the fallback; keep the fix centered
on selectBrowserTab and onBrowserTabSelect.

In `@src/renderer/hooks/coworking/useCoworkingRegistrySync.ts`:
- Around line 156-159: In useCoworkingRegistrySync, the session-id cache is
being updated before the IPC sync batch has actually succeeded, so a failed
removeSession path can leave stale tabs unaccounted for on the next retry. Move
the lastSessionIdsRef.current update so it only happens after the successful
batch completion, or save and restore the previous session-id set in the catch
block when the sync fails. Keep lastPayloadRef and the session-id cache in sync
with the actual outcome of the IPC operations.

---

Nitpick comments:
In `@src/renderer/components/coworking/CoworkingBackgroundBrowsers.tsx`:
- Around line 67-68: The inline ref callback on BrowserTabView is recreated on
every render, causing extra setHandle writes and Map churn even though
defaultResolveBackgroundHandle only polls asynchronously. Memoize the per-key
callback used in CoworkingBackgroundBrowsers so the same ref function is reused
for each m.key, and keep the existing setHandle behavior unchanged while
removing the unnecessary reattachment churn.

In `@src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts`:
- Around line 198-263: The approval prompt is happening before checking whether
the target browser tab is actually runnable, so non-focused ops can ask for
consent and then fail when background browsing is off. Update
useCoworkingBrowserResponder to resolve tab liveness/host availability first
using the existing isFocused and resolveBackgroundHandle flow, and only call
requestApproval for state-changing ops after confirming the op can execute. Keep
the early return path for the existing “Browser tab is not live” failure so
users are not prompted for impossible actions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c4db086-e1e7-420e-a403-e6918c24fbdc

📥 Commits

Reviewing files that changed from the base of the PR and between d3c279d and a5d7ea3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • package.json
  • src/__tests__/main/coworking/coworking-browser-bridge.test.ts
  • src/__tests__/renderer/hooks/coworking/applyBrowserOp.test.ts
  • src/__tests__/renderer/hooks/coworking/buildBrowserInputs.test.ts
  • src/__tests__/renderer/hooks/coworking/coworkingApproval.test.ts
  • src/__tests__/renderer/stores/coworkingBackgroundBrowserStore.test.ts
  • src/main/coworking/coworking-audit.ts
  • src/main/coworking/coworking-bridge.ts
  • src/main/coworking/coworking-registry.ts
  • src/main/coworking/coworking-server-script.ts
  • src/main/ipc/handlers/coworking.ts
  • src/main/preload/coworking.ts
  • src/main/stores/defaults.ts
  • src/renderer/components/MainPanel/BrowserTabView.tsx
  • src/renderer/components/MainPanel/MainPanel.tsx
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/components/coworking/CoworkingApprovalHost.tsx
  • src/renderer/components/coworking/CoworkingBackgroundBrowsers.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/stores/coworkingApprovalStore.ts
  • src/renderer/stores/coworkingBackgroundBrowserStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/index.ts
  • src/shared/coworkingBrowser.ts
  • src/shared/settingsMetadata.ts
💤 Files with no reviewable changes (1)
  • package.json

Comment thread src/main/coworking/coworking-audit.ts Outdated
Comment on lines +52 to +55
export function redactBrowserOpDetail(op: BrowserOp): string {
switch (op.kind) {
case 'navigate':
return `url=${op.url.slice(0, 200)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Strip query strings and fragments from audited navigate URLs.

Line 55 currently records op.url verbatim. OAuth callbacks, signed links, and app URLs routinely carry tokens or PII in the query/fragment, so this leaks secrets into both the Log Viewer and coworking-browser-audit.jsonl through Lines 73-79.

🔧 Suggested change
 export function redactBrowserOpDetail(op: BrowserOp): string {
 	switch (op.kind) {
-		case 'navigate':
-			return `url=${op.url.slice(0, 200)}`;
+		case 'navigate': {
+			try {
+				const url = new URL(op.url);
+				return `url=${`${url.origin}${url.pathname}`.slice(0, 200)}`;
+			} catch {
+				return 'url=[invalid]';
+			}
+		}
 		case 'click':
 			return `selector=${op.selector.slice(0, 120)}`;

Also applies to: 73-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/coworking/coworking-audit.ts` around lines 52 - 55, The
`redactBrowserOpDetail` helper is still logging `navigate` URLs with query
strings and fragments, which can leak tokens/PII into the audit output and JSONL
sink. Update the `navigate` branch to sanitize `op.url` before truncating it:
strip any search and hash components, then keep the existing 200-character
limit. Make the same redaction behavior apply wherever `BrowserOp` details are
serialized for the audit log so `coworking-browser-audit.jsonl` never receives
the full URL.

Source: Linters/SAST tools

Comment thread src/main/coworking/coworking-audit.ts Outdated
Comment on lines +77 to +79
try {
const file = path.join(app.getPath('userData'), 'coworking-browser-audit.jsonl');
fs.appendFileSync(file, JSON.stringify(entry) + '\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid synchronous audit writes on the Electron main thread.

Lines 77-79 call appendFileSync for every browser tool audit event. A chatty agent repeatedly hitting browser tools will block the main process on disk I/O and can stall IPC/UI responsiveness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/coworking/coworking-audit.ts` around lines 77 - 79, The audit write
in coworking-audit.ts is using a synchronous main-thread disk call that can
block Electron responsiveness. Update the audit path in the audit entry handling
logic to use asynchronous file writing instead of appendFileSync, ideally by
making the audit helper async and awaiting the write so browser tool events do
not stall the main process. Keep the same file target and JSONL format, but
ensure the write path in the audit function is non-blocking and that any callers
of the coworking audit entry flow handle the async result appropriately.

Comment on lines +416 to +424
const result = await auditedBrowserCall(
{
sessionId,
agentType,
tool: 'browser_interact',
opKind: op.kind,
detail: redactBrowserOpDetail(op),
},
() => browserInteract(sessionId, { id, op })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scrub sensitive URL components before audit logging.

browser_navigate audit detail flows through redactBrowserOpDetail(op), which currently truncates the URL but can still persist query strings or fragments containing tokens/PII. Log origin/path or explicitly strip search/hash before writing audit entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/coworking/coworking-bridge.ts` around lines 416 - 424, The audit
detail for browser navigation is still exposing sensitive URL parts, so update
redactBrowserOpDetail(op) to fully scrub browser_navigate payloads before
auditedBrowserCall logs them. Ensure the URL is normalized to origin/path only,
or explicitly remove search and hash from the op detail before it reaches the
audit entry. Keep the change localized around redactBrowserOpDetail and the
browser_interact/auditedBrowserCall flow so all navigation audits use the
sanitized form.

Comment on lines +1097 to +1098
<CoworkingApprovalHost theme={theme} />
<CoworkingBackgroundBrowsers theme={theme} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mount the approval host outside early-return panel branches.

These hosts are only rendered in the normal session view. If a browser interaction request arrives while Log Viewer, Agent Sessions, Memory Viewer, or the empty state is rendered, the approval queue has no modal host and the tool call waits until timeout. Move CoworkingApprovalHost to a parent that survives those branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/MainPanel/MainPanel.tsx` around lines 1097 - 1098,
The approval host is currently mounted only inside the normal session view, so
requests can’t surface a modal when MainPanel takes an early-return branch like
Log Viewer, Agent Sessions, Memory Viewer, or the empty state. Move
CoworkingApprovalHost out of the conditional render path in MainPanel so it
mounts in a parent scope that always survives those branches, while keeping
CoworkingBackgroundBrowsers where appropriate.

Addresses PR review findings (reprioritized around the real risk surface:
the agent pulling untrusted content into a privileged webview).

Browser-tool safety:
- eval now always requires per-call approval, even under confirm policy
  'off'; type is added to the default 'dangerous' confirm set.
- navigate is restricted to http/https/about:blank, blocking file://
  (local-file read-back), javascript: and data: (a second eval path).
- read_browser applies a default char cap (and a hard ceiling on an
  explicit maxChars); read_terminal clamps requested lines. Full sizes
  are still reported via totalChars/totalLines + truncated.

Correctness:
- serialize the registry-sync runs so an older async push can no longer
  overwrite a newer one and leave the main-process registry stale.
- protect a background browser tab from LRU eviction while it has an op
  in flight, so a concurrent cross-session mount can't unmount its
  webview mid-run.

Hygiene:
- audit log records navigate URLs origin+path only (query/fragment,
  where tokens live, stripped); the JSONL sink is created 0600.
- all four MCP installers now write config atomically (temp + rename)
  via a shared atomicWriteFile, so a crash mid-write can't truncate the
  user's ~/.claude.json / config.toml / opencode.json / mcp.json.
- lock the bridge socket to 0600 on POSIX.
- remove the unused upsertTerminal/removeTerminal IPC + preload surface
  (registry methods kept; they are the test seam).
- reuse getWhichCommand(); replace em-dashes in coworking source;
  add a searchable entry + data-setting-id for background browsing.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/coworking/pid-resolution.ts (1)

19-70: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Replace the Windows wmic fallback — WMIC is disabled by default on Windows 11 23H2/24H2, so stock installs will hit the catch path and reject the ppid handshake. Use Get-CimInstance (or another maintained resolver) as the primary path and keep wmic only as a legacy fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/coworking/pid-resolution.ts` around lines 19 - 70, The Windows
parent-PID lookup in getParentPid currently relies on wmic, which is no longer
available on many stock Windows installs and causes the handshake path to fail.
Update the Windows branch in src/main/coworking/pid-resolution.ts to use a
maintained resolver such as Get-CimInstance as the primary mechanism, and keep
wmic only as a legacy fallback for older systems. Preserve the existing
validation and null-on-failure behavior in getParentPid while ensuring the
Windows path succeeds on current Windows versions.
🧹 Nitpick comments (1)
src/main/utils/atomic-json-store.ts (1)

75-86: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

options.mode won't be applied if a stale .tmp file already exists.

fs.writeFile's mode option only takes effect when the file is newly created; if ${filePath}.tmp survives from a prior crash/interrupted write (or was previously written without mode), this call truncates it in place and keeps its existing permission bits, silently skipping the intended restriction for permission-sensitive payloads.

🔧 Proposed fix
 	const tmp = `${filePath}.tmp`;
-	await fs.writeFile(
-		tmp,
-		contents,
-		options?.mode !== undefined ? { encoding: 'utf-8', mode: options.mode } : 'utf-8'
-	);
+	await fs.writeFile(tmp, contents, 'utf-8');
+	if (options?.mode !== undefined) {
+		await fs.chmod(tmp, options.mode);
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/utils/atomic-json-store.ts` around lines 75 - 86, The
atomicWriteFile flow is reusing a pre-existing temporary file, so fs.writeFile
may ignore options.mode when ${filePath}.tmp already exists. Update
atomicWriteFile to ensure the temp path is always created with the intended
permissions before writing, either by removing any stale .tmp file or by opening
it in a way that forces a new file with the requested mode. Keep the fix
localized to atomicWriteFile and preserve the existing rename-based atomic swap
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/coworking/pid-resolution.ts`:
- Around line 19-70: The Windows parent-PID lookup in getParentPid currently
relies on wmic, which is no longer available on many stock Windows installs and
causes the handshake path to fail. Update the Windows branch in
src/main/coworking/pid-resolution.ts to use a maintained resolver such as
Get-CimInstance as the primary mechanism, and keep wmic only as a legacy
fallback for older systems. Preserve the existing validation and null-on-failure
behavior in getParentPid while ensuring the Windows path succeeds on current
Windows versions.

---

Nitpick comments:
In `@src/main/utils/atomic-json-store.ts`:
- Around line 75-86: The atomicWriteFile flow is reusing a pre-existing
temporary file, so fs.writeFile may ignore options.mode when ${filePath}.tmp
already exists. Update atomicWriteFile to ensure the temp path is always created
with the intended permissions before writing, either by removing any stale .tmp
file or by opening it in a way that forces a new file with the requested mode.
Keep the fix localized to atomicWriteFile and preserve the existing rename-based
atomic swap behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6407048b-a07d-4c0c-9144-966446c179d8

📥 Commits

Reviewing files that changed from the base of the PR and between a5d7ea3 and 74e322f.

📒 Files selected for processing (24)
  • src/main/coworking/coworking-audit.ts
  • src/main/coworking/coworking-bridge.ts
  • src/main/coworking/coworking-installer.ts
  • src/main/coworking/coworking-registry.ts
  • src/main/coworking/coworking-server-paths.ts
  • src/main/coworking/coworking-server-script.ts
  • src/main/coworking/coworking-session-id.ts
  • src/main/coworking/coworking-tools.ts
  • src/main/coworking/installers/claude-code.ts
  • src/main/coworking/installers/codex.ts
  • src/main/coworking/installers/factory-droid.ts
  • src/main/coworking/installers/opencode.ts
  • src/main/coworking/pid-resolution.ts
  • src/main/ipc/handlers/coworking.ts
  • src/main/preload/coworking.ts
  • src/main/utils/atomic-json-store.ts
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/components/Settings/searchableSettings.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts
  • src/renderer/hooks/coworking/useCoworkingBufferResponder.ts
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
  • src/renderer/stores/coworkingBackgroundBrowserStore.ts
  • src/shared/coworkingBrowser.ts
💤 Files with no reviewable changes (3)
  • src/renderer/global.d.ts
  • src/main/ipc/handlers/coworking.ts
  • src/main/preload/coworking.ts
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/components/Settings/searchableSettings.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/main/coworking/coworking-server-paths.ts
  • src/main/coworking/installers/claude-code.ts
  • src/main/coworking/installers/codex.ts
  • src/main/coworking/coworking-audit.ts
  • src/main/coworking/coworking-installer.ts
  • src/main/coworking/installers/opencode.ts
  • src/renderer/components/Settings/CoworkingSetup.tsx
  • src/renderer/hooks/coworking/useCoworkingRegistrySync.ts
  • src/renderer/hooks/coworking/useCoworkingBrowserResponder.ts
  • src/main/coworking/installers/factory-droid.ts
  • src/main/coworking/coworking-bridge.ts
  • src/main/coworking/coworking-registry.ts
  • src/main/coworking/coworking-server-script.ts

eval is now force-confirmed regardless of policy (even 'off'), and
type is part of the default 'dangerous' confirm set. Aligns the two
browserOpNeedsConfirm unit tests with the shipped behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jSydorowicz21 added a commit to jSydorowicz21/Maestro that referenced this pull request Jul 2, 2026
…ng branch

Merges fork-chr1syy/feat/agentic-coworking (74e322f harden + ca5bb60
tests) into our superseding branch. Both branches independently hardened the
same coworking review surface; reconciled hunk-by-hunk rather than taking
either side wholesale.

Brought in from upstream (additive):
- eval force-confirm regardless of policy (FORCE_CONFIRM_KINDS); type added
  to the dangerous confirm set
- navigate scheme allowlist (http/https/about:blank only) — ALSO applied to
  our newTab, closing a file:// local-read exfiltration path
- read caps: read_browser default+ceiling char cap, read_terminal line clamp
  (totalChars/totalLines still report full size)
- registry-sync run serialization; background-tab LRU eviction guard
- atomic installer writes (temp+rename); 0600 on bridge socket + audit JSONL
- searchable-settings entries + data-setting-id for background browsing

Reconciled (both touched — kept superior):
- audit URL redaction: kept ours (origin+path + queryChars/hashChars +
  <non-url textChars>); adopted their 0600 sink; deleted their now-dead
  redactAuditUrl
- confirm policy: UNION dangerous set = navigate/eval/type/newTab/closeTab;
  adopted their FORCE_CONFIRM_KINDS + restructured browserOpNeedsConfirm; kept
  our BrowserOp additions (waitFor/newTab/closeTab/read.selector)
- registry-sync: their serialization + our cache-restore-on-failure +
  confirmPolicy/hiddenFromAgent push
- CoworkingSetup: kept our redesign, grafted their data-setting-id; relabeled
  confirm 'Never'->'JS only' and fixed descriptions for eval-force-confirm

Preserved (ours only): ephemeral tabs, clearSessionData, main-side hidden
enforcement, confirm mirror + boundary recheck, wmic->PowerShell-CIM fallback.

Verified: typecheck (lint+main) green; coworking/settings slice 545 tests
green (Tester reconciled the confirm contract + read caps, both polarities);
MCP stdio smoke SMOKE-PASS (17 tools, newTab/waitFor/selector forwarded).
@chr1syy

chr1syy commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #1160 (feat/agentic-coworking-hardened), which carries this work forward with browser-tooling hardening, session-persistence controls, and additional agent tools. The outstanding review threads here (async pid resolution to avoid blocking the Electron main loop, URL scrubbing in the browser audit log, async audit writes) are being addressed in #1160. No need to keep two coworking branches open.

@chr1syy chr1syy closed this Jul 2, 2026
jSydorowicz21 added a commit that referenced this pull request Jul 4, 2026
…ols, new agent tools

Fixes from the #948 feature-completeness vet:
- Ephemeral (incognito) browser tabs: maestro-ephemeral-<sid>-<rand8> in-memory
  partition, New Incognito Browser entry, tab-strip badge, never persisted/restored;
  window-manager allowlist extended deliberately
- Clear-browsing-data: hardened browser:clearSessionData IPC (full-pattern partition
  validation + window-sender gate) with two-step confirm toolbar button
- hiddenFromAgent now crosses IPC and is enforced in the MAIN registry (list AND
  resolve; hidden == not-found) plus an execution-boundary re-check in the renderer;
  browser:N ids stay stable across hide/unhide
- Per-agent confirm policy mirrored to main; main computes needsConfirm per op and
  the renderer ORs it with local policy (stale settings can never weaken the gate)
- Audit log: navigate/newTab URLs stripped to origin+path (query/hash -> char
  counts), non-URL targets length-only; terminal tools now audited too
- Windows ppid walk falls back wmic -> PowerShell CIM with backend caching (wmic is
  absent on newer Win11 builds; validated live on this host)

New agent tools (17 total advertised):
- browser_wait_for(id, selector, timeoutMs<=30s) - host-side element polling
- browser_new_tab(url?, ephemeral?) - session-scoped, URL normalized like navigate,
  always confirm-gated; browser_close_tab(id) - confirm-gated store close
- read_browser gains selector for element-scoped reads

Verified: typecheck green (lint+main tsconfigs); 25 test files / 449 tests green
(54 new/rewritten by test pass); generated MCP server script node --check clean and
driven end-to-end over stdio against a mock bridge (SMOKE-PASS); codex review run,
2 findings fixed, 2 declined with rationale
jSydorowicz21 added a commit that referenced this pull request Jul 5, 2026
…rdened

feat(coworking): browser tooling hardening, session-persistence controls, new agent tools (supersedes #948)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocking Can't move forward due to some hurdle.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants