Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

### Background Chrome without focus stealing

- Routed the legacy low-level `tabs.open` primitive through `workspace.open` at the client, native-host, and extension layers. Older/stale sessions can no longer create loose Chrome tabs outside `MDB`; if the workspace is missing they fail closed until the group is auto-healed. The extension now recreates the default four-tab workspace whenever Chrome is naturally focused, avoiding manual re-setup after browser/extension restarts.
- Moved `chrome_click` execution into the page MAIN world so framework handlers receive page-world event objects, added an accessible `ArrowDown` fallback for semantic comboboxes that remain closed after pointer/mouse activation, and added an internal grantless `extension.reload` maintenance hook so future unpacked-extension code updates can reload without another manual `chrome://extensions` click.
- Made `chrome_click` adaptive for mousedown-driven combobox/menu controls: if `mousedown` is consumed or changes popup/expanded state, MDB stops after pointer-up/mouse-up instead of issuing a redundant `.click()` that can toggle the control closed. `chrome_snapshot` now exposes `aria-expanded`, `aria-haspopup`, `aria-controls`, and `data-state` for interaction diagnostics.
- Upgraded `chrome_click` from bare DOM `.click()` to a pointer/mouse event sequence including `pointerdown`, `mousedown`, focus, `pointerup`, and `mouseup` before native click activation. This supports modern custom controls that listen earlier in the interaction cycle while preserving the boundary that synthetic events are not trusted user gestures.
- Raised the managed `MDB` pool target from four to eight tabs and made contention resilient: opens wait up to 20 seconds for a release, active browser operations renew a lease heartbeat, and leases idle for 10 minutes are reclaimed/reset. Existing smaller pools expand only when Chrome is naturally foreground, preserving the no-focus-steal rule.
- Added read-only `chatgpt_extension_status`, which reports the installed ChatGPT Chrome extension, its local OpenAI native-host registration, and the live `chatgpt.com` page-bridge status when available without patching the OpenAI extension or exposing its private native-host RPC protocol.
- Made workspace release grantless so Strict-mode URL approval expiry cannot strand a finished lease.
- Routed the legacy low-level `tabs.open` primitive through `workspace.open` at the client, native-host, and extension layers. Older/stale sessions can no longer create loose Chrome tabs outside `MDB`; if the workspace is missing they fail closed until the group is auto-healed. The extension now recreates the managed workspace whenever Chrome is naturally focused, avoiding manual re-setup after browser/extension restarts.
- Separated approval strictness from Chrome routing. Direct Chrome AppleScript/JXA, direct Chrome executable launches, and shell `open` calls for web URLs (including `open -g`) are always refused with `CHROME_BACKGROUND_REQUIRED` in both Relaxed and Strict modes, forcing browser work through the signed-in `MDB` tab group. This fixes a regression where Relaxed mode let other sessions bypass the extension, create ungrouped tabs, and steal foreground focus.
- Fixed test isolation so the federation/installer suites cannot target the live checkout or inherit/remove the running menu-bar bridge unlock file. Running `npm test` no longer causes the live MDB endpoint to fall into 502 afterward.
- Changed the product default to **relaxed approvals**: the signed-in MDB Chrome workspace can use normal HTTP/HTTPS sites and native foreground app control can execute without per-site/per-app terminal approval commands. Added a live **Strict approvals** checkbox to the menu-bar app; when enabled it restores the scoped background-Chrome grant pool and one-use foreground-app approvals.
- Added an optional Manifest V3 Chrome extension plus native-messaging host so ChatGPT can operate approved pages in the operator's real signed-in Chrome profile without routine focus theft. New built-in tools are `chrome_workspace_status`, `chrome_workspace_setup`, `chrome_tabs`, `chrome_open`, `chrome_navigate`, `chrome_snapshot`, `chrome_click`, `chrome_fill`, and `chrome_close`.
- Bound the native host to the selected signed-in Chrome profile/account and fail closed on signed-out or mismatched profiles.
- Added a Chrome-native **`MDB`** tab group with a reusable background-tab pool (four tabs by default, up to eight), per-tab leases, idle collapse, restart reconciliation, and group rediscovery. Routine `chrome_open` leases a pre-created tab rather than creating one; `chrome_close` returns it to the pool.
- Added a Chrome-native **`MDB`** tab group with a reusable background-tab pool (eight tabs by default and maximum), per-tab leases, idle collapse, restart reconciliation, and group rediscovery. Routine `chrome_open` leases a pre-created tab rather than creating one; `chrome_close` returns it to the pool.
- Made workspace status/setup local and grantless. Reworked authenticated `chrome-background` approval into a shared additive grant pool: concurrent ChatGPT sessions can approve different domains without replacing one another, URL scopes are unioned while each grant remains unexpired, and active grants survive bridge-child restarts until their original expiry. Legacy fixed-file approvals are imported for backward compatibility. Federated personal-browser providers keep their single-use semantics.
- Refuse workspace creation/expansion unless Chrome is already focused. This is deliberate: measured on Chrome 151/macOS, even `tabs.create({active:false})` can bring Chrome to the foreground. Routine work therefore performs no tab creation after the one-time setup.
- Added background-first desktop GUI detection for native apps such as Slack. Relaxed mode still permits non-Chrome foreground UI when genuinely required; Strict mode requires a single-use, app-scoped, maximum-five-minute grant. The old model-controlled env bypass is gone, while Chrome is always forced through the separate `MDB` background routing rule above.
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ Git, package managers, Vercel CLI, database CLIs, AppleScript, browser CLIs, bui
| Tool | Purpose |
|---|---|
| `bridge_status` | Runtime identity, paths, permissions context, shell, audit mode, Codex binary, focus policy, and background-Chrome status |
| `chrome_workspace_status` | Inspect the extension-owned `MDB` Chrome group and reusable background-tab pool; no website grant required |
| `chrome_workspace_setup` | Create or expand the `MDB` pool once while Chrome is already foreground |
| `chrome_workspace_status` | Inspect the extension-owned `MDB` Chrome group, lease activity, and reusable background-tab pool; no website grant required |
| `chatgpt_extension_status` | Inspect the installed ChatGPT Chrome extension, OpenAI native-host registration, and live read-only page-bridge status without patching the OpenAI extension |
| `chrome_workspace_setup` | Create or expand the `MDB` pool while Chrome is already foreground; default target is eight reusable tabs |
| `chrome_tabs` | List tabs in the real signed-in Chrome profile without activating Chrome; scoped only when Strict approvals is on |
| `chrome_open` | Lease an idle tab from the persistent `MDB` group and open a URL without creating a new tab |
| `chrome_navigate` | Navigate an approved tab without selecting it |
Expand Down Expand Up @@ -127,11 +128,11 @@ This is intentionally opt-in because authenticated browser control is powerful.

Then in Chrome open `chrome://extensions`, enable **Developer mode**, choose **Load unpacked**, and select this repository's `chrome-extension/` directory. The expected extension id is `pcebfblnmcappinbenkmddjdapaoajgm`.

The extension keeps a Chrome-native tab group named **`MDB`**. By default it contains four extension-owned idle tabs. They are created only while Chrome is already foreground, then leased and reused for routine work. The group is collapsed when idle and expands while one or more tabs are leased. This mirrors the managed-group approach used by browser-agent extensions while avoiding a macOS/Chrome quirk measured on this project: even `chrome.tabs.create({ active:false })` can bring Chrome to the foreground.
The extension keeps a Chrome-native tab group named **`MDB`**. By default it targets eight extension-owned idle tabs. They are created only while Chrome is already foreground, then leased and reused for routine work. The group is collapsed when idle and expands while one or more tabs are leased. This mirrors the managed-group approach used by browser-agent extensions while avoiding a macOS/Chrome quirk measured on this project: even `chrome.tabs.create({ active:false })` can bring Chrome to the foreground.

The pool now self-heals. If Chrome or the extension restarts and the `MDB` group is missing, the extension recreates the default four-tab pool the next time you **naturally focus Chrome**. It never activates Chrome just to repair itself. You can also force setup while Chrome is already foreground by calling `chrome_workspace_setup` (default pool size: 4).
The pool now self-heals and self-expands. If Chrome or the extension restarts, or an older four-tab pool is still present, the extension grows the managed pool to the default eight tabs the next time you **naturally focus Chrome**. It never activates Chrome just to repair or expand itself. You can also force setup while Chrome is already foreground by calling `chrome_workspace_setup` (default pool size: 8).

`chrome_workspace_status` is grantless because it only reads extension-owned local workspace state. `chrome_workspace_setup` is also grantless because it creates only extension-owned idle pages; it refuses to create or expand the pool unless Chrome is already focused rather than stealing focus itself. Legacy/internal `tabs.open` callers are routed to the same `workspace.open` lease path, so they cannot create loose tabs outside `MDB`; if the pool is unavailable while Chrome is background, the open fails closed until the group can be repaired.
`chrome_workspace_status` is grantless because it only reads extension-owned local workspace state. It now includes lease age/idle metadata, the 10-minute idle-reclaim timeout, and the 20-second lease-wait budget. `chrome_workspace_setup` is also grantless because it creates only extension-owned idle pages; it refuses to create or expand the pool unless Chrome is already focused rather than stealing focus itself. Legacy/internal `tabs.open` callers are routed to the same `workspace.open` lease path, so they cannot create loose tabs outside `MDB`. When all tabs are busy, `chrome_open` waits briefly for a release instead of failing immediately; abandoned leases are reclaimed after 10 minutes without browser activity, while every navigate/snapshot/click/fill renews an active lease.

**Relaxed access is the default.** Normal HTTP/HTTPS work through the signed-in `MDB` Chrome profile does not require a terminal approval command or per-site allowlist. This is intentional: Mac Developer Bridge already exposes unrestricted shell/file authority as the logged-in macOS user, and the useful default is for browser execution to match that operator-chosen trust level while remaining background-first.

Expand All @@ -152,10 +153,13 @@ A normal workflow is:
1. `chrome_open` an approved URL into an idle tab leased from the `MDB` group.
2. `chrome_snapshot` to read the page and get stable-enough selectors for visible controls.
3. `chrome_fill` / `chrome_click` / `chrome_navigate` as needed.
4. `chrome_close` to return the workspace tab to its idle extension page and release the lease.
4. `chrome_close` to return the workspace tab to its idle extension page and release the lease. Workspace release is local/grantless cleanup, so Strict-mode URL grants cannot strand a finished lease.

Profile binding is always enforced. In relaxed mode the extension permits normal HTTP/HTTPS sites without a per-site grant. In Strict mode, each `chrome-background` approval is stored as its own mode-0600 file under `$DATA_DIR/chrome-background-grants/`, expires after at most 15 minutes, and is merged with other still-live approvals. Expired files are pruned automatically and URL patterns are enforced inside Chrome. Federated personal-browser providers keep their separate single-use behavior.


`chatgpt_extension_status` is deliberately read-only. It reports the installed ChatGPT Chrome extension version, the local `com.openai.codexextension` native-host registration, and—when a `chatgpt.com` tab is already open—the live status returned by OpenAI's own page bridge. MDB does **not** patch the OpenAI extension, add itself to the OpenAI native-host allowlist, expose arbitrary private OpenAI RPC calls, or programmatically open the ChatGPT side panel. The current ChatGPT extension does not declare `externally_connectable`; its side-panel open path also requires a trusted user gesture.

What background mode does **not** promise: CAPTCHAs, native browser/OS permission dialogs, file pickers, downloads requiring a trusted user gesture, passkeys, and other browser security UI may require a foreground/manual step. The bridge reports that limitation rather than silently activating Chrome. This is also deliberately narrower than arbitrary page JavaScript or network-header capture; see [SECURITY.md](SECURITY.md).

To remove the integration:
Expand Down
11 changes: 8 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,18 @@ The built-in background surface is intentionally narrower than the federated Chr
- no file-upload tool or native file picker;
- password input values are returned as `<redacted>` by `chrome_snapshot`;
- authenticated web actions are tab list/open/navigate/snapshot/click/fill/close;
- `chrome_workspace_status` and `chrome_workspace_setup` manage only extension-owned local workspace state and do not consume a website grant;
- `chrome_workspace_status`, `chrome_workspace_setup`, workspace release, and `chatgpt_extension_status` are local/read-only-or-cleanup operations and do not consume a website grant;
- routine `chrome_open` **does not create a Chrome tab**. It leases one of the pre-created extension-owned tabs from the `MDB` group;
- `chrome_close` releases an `MDB` tab back to its idle extension page instead of destroying the pool tab.

The `MDB` group is a Chrome-native tab group backed by `chrome.storage.local`. The default pool size is four (maximum eight). The group is collapsed when idle and expands while tabs are leased. State is reconciled after extension/service-worker restarts, and the extension can rediscover its own group by title/color plus the presence of an extension-owned workspace page. It deliberately does not adopt an arbitrary user group that merely happens to have the same title. Older/internal `tabs.open` requests are normalized to `workspace.open` in the JS client, native host, and extension so there is no supported loose-tab creation path.
The `MDB` group is a Chrome-native tab group backed by `chrome.storage.local`. The default and maximum managed pool size is eight. The group is collapsed when idle and expands while tabs are leased. State is reconciled after extension/service-worker restarts, and the extension can rediscover its own group by title/color plus the presence of an extension-owned workspace page. It deliberately does not adopt an arbitrary user group that merely happens to have the same title. Older/internal `tabs.open` requests are normalized to `workspace.open` in the JS client, native host, and extension so there is no supported loose-tab creation path.

Pool creation remains a foreground-only boundary. On the measured Chrome/macOS combination, even `chrome.tabs.create({active:false})` can foreground Chrome. `chrome_workspace_setup` therefore refuses to create or expand the pool unless a normal Chrome window is **already focused**, and routine browser work fails closed rather than creating a loose fallback tab. If the pool disappears after a Chrome/extension restart, the extension automatically recreates the default pool the next time Chrome becomes naturally focused; it never activates Chrome on the operator's behalf. Once the pool exists, routine open/navigate/read/click/fill/release operations reuse those tabs and avoid creation-time focus theft.
Pool creation and expansion remain a foreground-only boundary. On the measured Chrome/macOS combination, even `chrome.tabs.create({active:false})` can foreground Chrome. `chrome_workspace_setup` therefore refuses to create or expand the pool unless a normal Chrome window is **already focused**, and routine browser work fails closed rather than creating a loose fallback tab. If the pool disappears after a Chrome/extension restart, or an older pool has fewer than eight tabs, the extension automatically creates/expands the default pool the next time Chrome becomes naturally focused; it never activates Chrome on the operator's behalf. Once the pool exists, routine open/navigate/read/click/fill/release operations reuse those tabs and avoid creation-time focus theft.


Lease contention is handled as a scheduling problem rather than an immediate fatal error. An open waits up to 20 seconds for a free slot. A lease records both creation time and last browser activity; navigate/snapshot/click/fill renew that activity timestamp, while a lease idle for 10 minutes is reclaimed and its tab reset to the extension-owned idle page. This is crash/abandonment cleanup, not per-conversation ownership: `mcp-http.mjs` still multiplexes callers into one bridge child and the extension does not know a ChatGPT conversation identity.

`chatgpt_extension_status` does not create a cross-extension control channel. The installed OpenAI extension currently exposes no `externally_connectable` contract. MDB only reads local installation/native-host metadata and, when a `chatgpt.com` tab exists, asks the page bridge that OpenAI already injects for its read-only status event. MDB does not modify OpenAI's native-host allowlist or invoke arbitrary undocumented `com.openai.codexextension` JSON-RPC methods.

By default, background Chrome runs in **relaxed** approval mode: once the extension/profile binding is installed, normal HTTP/HTTPS URLs do not require per-site grant files. This matches the project's intentional unrestricted-shell trust model and removes approval ceremony from ordinary execution. The operator can enable **Strict approvals** in the menu-bar app at any time; the bridge re-reads `$DATA_DIR/settings.json` on each relevant action, so the change is live. In Strict mode, background-Chrome grants are additive mode-0600 files under `$DATA_DIR/chrome-background-grants/`, each with its own nonce, URL patterns, and expiry capped at 15 minutes. The bridge unions all unexpired patterns and Chrome remains the final URL authority.

Expand Down
Loading