feat(server): let users withhold browser access from agents#7083

merged4
main
browser-agent-access
openedAug 15, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Stacked on #7082 — review that first; this PR's base is browser-viewport-defaults.

Adds Settings → Integrations → Browser → Agent browser access, on by default. Agents could always drive the preview browser with no way to say no.

How the gate works

Turning it off withholds the MCP credential in ProviderService.prepareMcpSession. That's the only place a credential is minted, and /mcp accepts nothing else, so one branch covers all five providers and any external MCP client — every adapter already treats a missing session as "no MCP server".

The Codex developer instructions describing preview_* drop out along with the tools. That's derived from the session's actual MCP configuration via the existing hasConfiguredMcpServer, not a second read of the setting, so the prompt cannot describe tools the turn doesn't have.

That coupling matters more than it looks: the block explicitly steers the model away from Playwright and agent-browser. Leaving it in with the tools gone would talk the agent out of the only browser automation it has left. There's a test asserting the anti-steering line disappears with the tool names.

Two decisions worth a look

Denying on an unreadable settings file. Reading settings introduced ServerSettingsError into ProviderServiceError, a union every caller handles. Rather than widen it, the error is contained at the boundary and treated as deny. An explicit "off" silently becoming "on" would violate the user's choice; the reverse costs an agent one toolset and is visible immediately.

Scope is new sessions. Credentials live up to 24h and can't be rotated into an already-spawned agent process, so a running agent keeps its tools. The UI states this rather than implying otherwise. Closing it would mean revoking live credentials on the settings change — revokeAllActiveMcpCredentials exists, but there's no settings-change subscriber on the server today, so it needs a small daemon. Happy to add it if wanted.

Why this one stays enabled on web

Unlike the client-local defaults in #7082, this is a ServerSetting. The agent runs server-side and its preview calls route to whichever desktop client is attached, so the realistic case is: desktop app running with an agent working, you're on a phone or web tab, and you want to revoke access. That's exactly when you'd reach for it and least want it greyed out.

Testing

Typecheck and lint clean; full suite passing, including new coverage for the prompt gating. Verified in a running dev instance: toggle defaults on, flips, persists to settings.json, survives reload.

Note

Add user-controlled toggle to withhold browser access from agents

  • Adds enableAgentBrowserAccess to ServerSettings (default true), with a toggle in the Integrations settings UI under the Browser section.
  • When disabled, makeProviderService revokes any active MCP credential, clears the MCP provider session, and returns undefined for new sessions instead of issuing credentials.
  • Browser tool instructions are omitted from agent prompts when MCP server tools are unavailable, via a new browserToolsAvailable parameter threaded through buildCodexDeveloperInstructions.
  • The setting is searchable, restorable to defaults, and patchable via the server settings API.
  • Behavioral Change: disabling the setting actively revokes existing credentials mid-session, not just on new sessions.

Macroscope summarized 0d31f11.


Note

Medium Risk Changes MCP credential issuance and revocation at session prepare time and agent-facing Codex prompts; mistakes could leave stale tokens valid or mislead agents about available browser tools, though tests cover the gate and prompt omission.

Overview Introduces agent browser access as a server-authoritative setting (enableAgentBrowserAccess, default on). When off, ProviderService.prepareMcpSession skips issuing the t3-code MCP credential, revokes any existing token for that thread, and clears the in-memory MCP session so agents never get preview_* tools. Unreadable settings are treated as deny rather than widening ProviderServiceError.

Codex developer instructions are now built via functions that take browserToolsAvailable; the T3 preview browser block (including anti–Playwright steering) is omitted entirely when MCP is not configured on the session. CodexSessionRuntime passes hasConfiguredMcpServer(appServerArgs) into turn start so prompts match the tools on that turn.

Settings → Integrations → Browser adds a toggle (searchable, included in restore-defaults), with copy that already-running agents keep tools they were given. Contracts gain enableAgentBrowserAccess on server settings and patch types.

Reviewed by Cursor Bugbot for commit 0d31f1163ef219ee4489380e5e63400c77a4ddac. Bugbot is set up for automated code reviews on this repo. Configure here.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d90ac31b-137a-4d9e-a4d6-b7356f604cb0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

apps/server/src/provider/Layers/ProviderService.tslines 253–271Outdated · 33ee09bOpen thread
MmacroscopeappBoton lines 253–271Aug 15, 2026

🟠 High Layers/ProviderService.ts:253

When agent browser access is disabled for a thread, its previously issued MCP credential remains valid and /mcp continues accepting the old token until expiry. This branch skips issueActiveMcpCredential (the normal revocation path) and only clears McpProviderSession; revoke the thread with revokeActiveMcpThread(threadId) before clearing the provider session.

✅ Resolved in b4d14de34078cf5f87d6ff04c34667954e95b4e3

MmacroscopeappBotreviewed · 1 inline threadAug 15, 2026
Synced from GitHub

One finding: the new server-side gate on MCP credential issuance changes backend behavior but has no focused test. Everything else in the changed Effect code follows the conventions (namespace imports, yield* ServerSettings.ServerSettingsService for dependency acquisition, whole-channel Effect.catch).

Posted via Macroscope — Effect Service Conventions

apps/server/src/provider/Layers/ProviderService.tsline 250Outdated · 33ee09bOpen thread
……@@ -215,16 +216,52 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
226- ),
227- );
249+ Effect.gen(function* () {
250+ if (!(yield* agentBrowserAccessEnabled)) {
MmacroscopeappBoton line 250Aug 15, 2026

This is a backend behavior change (a session may now start with no MCP credential, and a stale provider session is cleared), but the only new tests cover prompt text in CodexSessionRuntime.test.ts. Consider adding a focused test in ProviderService.test.ts using ServerSettings.ServerSettingsService.layerTest({ enableAgentBrowserAccess: false }) asserting that startSession issues no credential and clears the thread's MCP provider session, plus the enabled counterpart — the existing suite already wires a test settings layer, so it is a small addition.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire12.7 KiB12.6 KiB−45 B (−0.3%)15.1 KiB✅
CodexThread snapshot wire6.3 KiB6.3 KiB+2 B (+0.0%)7.3 KiB✅
CodexLive turn WebSocket wire6.4 KiB6.3 KiB−47 B (−0.7%)7.8 KiB✅
CodexLive turn WebSocket decoded51.9 KiB51.8 KiB−44 B (−0.1%)66.4 KiB✅
CodexLive turn messages1716−1 (−5.9%)21✅
ClaudeTotal thread wire12.6 KiB12.6 KiB+5 B (+0.0%)15.1 KiB✅
ClaudeThread snapshot wire6.3 KiB6.3 KiB−4 B (−0.1%)7.3 KiB✅
ClaudeLive turn WebSocket wire6.3 KiB6.3 KiB+9 B (+0.1%)7.8 KiB✅
ClaudeLive turn WebSocket decoded52.7 KiB52.7 KiB0 B (0.0%)66.4 KiB✅
ClaudeLive turn messages16160 (0.0%)21✅

Baseline: 949feb6 · PR result: 0d31f11 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 99.9 KiB
  • Claude decoded thread snapshot: 100.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

apps/server/src/provider/Layers/ProviderService.tsline 254Outdated · 3587069Open thread
……@@ -215,16 +216,52 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
251+ // A stale session from before the toggle flipped would otherwise keep
252+ // its tools for the rest of its life.
253+ yield* Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId));
254+ return undefined;
CcursorBoton line 254Aug 15, 2026

Deny path skips credential revoke

High Severity

When enableAgentBrowserAccess is off, prepareMcpSession only clears the in-memory MCP map and never calls revokeActiveMcpThread. Before this change, every prepare path went through issueActiveMcpCredential, which always revokes the thread first. Session restarts (runtime mode, cwd, model) call startSession without stopSession, so they relied on that revoke. Orphaned bearer tokens stay valid, and touchActiveMcpThread on later turns refreshes them for up to the full liveness window—longer if turns keep coming. If replacing an old process fails, that agent can keep driving /mcp after the user withheld access.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 358706969b796ffbc757b5f50d68f61e2f778f24. Configure here.

Synced from GitHub

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (agent browser access control) with schema additions, UI components, and credential management logic. Two Medium severity findings remain unresolved: a potential config source mismatch in CodexSessionRuntime.ts and a restore-defaults race condition. Human review warranted for new capability and blocking findings.

You can customize Macroscope's approvability policy. Learn more.

apps/server/src/provider/Layers/CodexSessionRuntime.tsline 1833Outdated · 093a16dOpen thread
……@@ -1822,6 +1827,10 @@ export const makeCodexSessionRuntime = (
1830+ // Derived from the session's own MCP configuration rather than the
1831+ // setting, so the prompt describes the tools this turn actually
1832+ // has even if the setting changed after the session started.
1833+ browserToolsAvailable: hasConfiguredMcpServer(options.appServerArgs),
MmacroscopeappBoton line 1833Aug 17, 2026

🟡 Medium Layers/CodexSessionRuntime.ts:1833

When MCP is configured through launchArgs, the spawned session has the preview_* tools but the generated developer instructions omit them because this check reads only options.appServerArgs. Use the resolved appServerArgs value here.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/provider/Layers/CodexSessionRuntime.ts around line 1833:
2
3When MCP is configured through `launchArgs`, the spawned session has the `preview_*` tools but the generated developer instructions omit them because this check reads only `options.appServerArgs`. Use the resolved `appServerArgs` value here.
CcursorBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit 093a16d6a426c5340b4016ec16b7c9141efbb7bf. Configure here.

apps/web/src/components/settings/IntegrationsSettings.tsxline 381Outdated · 093a16dOpen thread
……@@ -350,6 +355,44 @@ function BrowserAppearanceSetting({ disabled }: { readonly disabled: boolean })
378+ })
379+ }
380+ />
381+ ) : null
CcursorBoton line 381Aug 17, 2026

Restore defaults skips agent browser access

Medium Severity

enableAgentBrowserAccess has a per-row reset on Integrations, but General → Restore defaults never dirty-tracks or resets it. Peer server toggles like enableProviderUpdateChecks and the other Browser defaults are included. After Restore defaults, agent browser access can stay off with no mention in the confirmation list.

Triggered by learned rule: Restore defaults on General settings page is intentionally global — not scoped per-page

Reviewed by Cursor Bugbot for commit 093a16d6a426c5340b4016ec16b7c9141efbb7bf. Configure here.

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review of the settings changes. One finding: the new Agent browser access row advertises default semantics with a per-row reset button, but it is not wired into the page-level Restore defaults flow that the rest of this Browser section uses. Everything else (SettingsRow/SettingResetButton/Switch composition, search catalog entry, sidebar nav derivation, desktop-only framing) matches the existing settings primitives.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 371–382Outdated · 093a16dOpen thread
……@@ -350,6 +355,44 @@ function BrowserAppearanceSetting({ disabled }: { readonly disabled: boolean })
379+ }
380+ />
381+ ) : null
382+ }
MmacroscopeappBoton lines 371–382Aug 17, 2026

This row offers a per-setting reset, but enableAgentBrowserAccess isn't part of the page-level Restore defaults flow. getChangedBrowserSettingLabels (SettingsPanels.logic.ts) and the restoreDefaults patch (SettingsPanels.tsx) cover the four browser client defaults in this same section but not this toggle, so "Restore defaults" leaves agent access off and never mentions it in the confirmation dialog — inconsistent with every other reset-able row here.

Smallest fix: add enableAgentBrowserAccess to the changed-label list (e.g. "Agent browser access", plus the memo dependency) and to the updateSettings({ ... }) restore patch. If bulk restore should deliberately not re-grant agent access, a one-line comment stating that would keep the exception intentional.

Posted via Macroscope — UI Consistency

Added 4 commits

  • fix(web): include agent browser access in Restore defaults0d31f11
  • fix(server): revoke the MCP credential when agent browser access is withheld324e3be
  • feat(server): let users withhold browser access from agents5e93541
  • test(server): cover the agent browser access gate9989fdf
apps/web/src/browser/browserDefaults.tsline 66Outdated · 90cc93cOpen thread
……@@ -0,0 +1,114 @@
63+ return getBrowserDefaults();
64+}
65+
66+export function useBrowserDefaults(): BrowserDefaults {

🟡 Medium browser/browserDefaults.ts:66

When PreviewView toggles the device toolbar during startup, useBrowserDefaults returns the schema-default viewport and the toggle commits that built-in viewport instead of the user's persisted browser default; the viewport is not corrected after hydration. Unlike the imperative accessors, this hook is not hydration-gated, so make the toggle wait for client-settings hydration (or expose hydration-aware hook state) before applying the default.

No longer relevant as of 0d31f1163ef219ee4489380e5e63400c77a4ddac

apps/web/src/components/settings/SettingsPanels.tsxline 671Outdated · 90cc93cOpen thread
……@@ -658,6 +668,14 @@ export function useSettingsRestore(onRestored?: () => void) {
658668 fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt,
659669 fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode,
660670 fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal,
671+ browserDefaultViewport: DEFAULT_UNIFIED_SETTINGS.browserDefaultViewport,
MmacroscopeappBoton line 671Aug 17, 2026

🟡 Medium settings/SettingsPanels.tsx:671

Confirming an unrelated restore silently resets browser viewport, zoom, appearance, or auto-show preferences changed in another tab while the dialog was open, even though those settings were not listed for restoration. updateSettings always writes all four browserDefault* fields after confirmation; re-read the live settings or update only fields included in the confirmed dirty snapshot.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/SettingsPanels.tsx around line 671:
2
3Confirming an unrelated restore silently resets browser viewport, zoom, appearance, or auto-show preferences changed in another tab while the dialog was open, even though those settings were not listed for restoration. `updateSettings` always writes all four `browserDefault*` fields after confirmation; re-read the live settings or update only fields included in the confirmed dirty snapshot.
MmacroscopeappBotreviewed · 2 inline threadsAug 17, 2026
Synced from GitHub

Reviewed the new Integrations settings UI and the browser-default call sites in apps/web/src. Two consistency findings in IntegrationsSettings.tsx, both about the viewport row's controls; the rest of the panel (SettingsRow/SettingsSection composition, shared Select/Switch/Button primitives, searchableSetting anchors, reused ScreenRotationIcon) matches the existing settings pages.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 224–224Outdated · 90cc93cOpen thread
……@@ -0,0 +1,482 @@
221+ format={NO_GROUPING}
222+ size="sm"
223+ className="w-20"
224+ onValueCommitted={(value) => commitDimension("width", value)}
MmacroscopeappBoton lines 224–224Aug 17, 2026

NumberField is fully controlled here (value passed, no onValueChange), so value changes the primitive initiates itself — ArrowUp/ArrowDown stepping, scroll-wheel — are dropped: the parent never echoes them, and with no increment/decrement parts rendered, typing is the only way to change the size. Every other NumberField call site in the app pairs value with onValueChange for this reason.

Suggest keeping a local draft via onValueChange and persisting in onValueCommitted, i.e. the same draft-then-commit shape BrowserDeviceToolbar uses for the identical width/height pair — and mirroring its aria-invalid so a size commitDimension rejects (over PREVIEW_VIEWPORT_MAX_AREA) isn't silently ignored while the input keeps showing it.

Posted via Macroscope — UI Consistency

apps/web/src/components/settings/IntegrationsSettings.tsxlines 183–183Outdated · 90cc93cOpen thread
……@@ -0,0 +1,482 @@
180+ control={
181+ <div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
182+ <Select
183+ value={viewportSelectValue(viewport)}
MmacroscopeappBoton lines 183–183Aug 17, 2026

This row's select is one size step shorter than the rows directly below it. size="sm" is min-h-8 sm:min-h-7, while every other select rendered inside a SettingsRow (including BrowserZoomSetting and BrowserAppearanceSetting a few lines down, and the Appearance/General panels) uses the default min-h-9 sm:min-h-8, so the Browser section ends up with two different control heights stacked on top of each other.

Suggest dropping size="sm" here and from the two NumberFields below it (their sm height is what currently lines up with this trigger), so the whole section shares the standard control height.

Posted via Macroscope — UI Consistency