Audit metadata
- Priority: P1
- Estimated effort: L
- Implementation risk: MED
- Category: bug
- Evidence baseline:
07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)
Dependencies
- P1 proposal 041 — Use one protected browser surface for users and agents. Included in the current P1 issue wave.
Description and impact
Pi already sends a browser session ID, but the server discards it and routes all
users/tabs to one global page and fallback URL. Concurrent sessions can navigate,
read, click, type into, screenshot, or inherit cookies from each other's page.
Session-scoped pages and Chromium contexts are required for correctness and
privacy once the visible and agent browser share one host.
Current state
frontend/desktop/resources/pi-extensions/browser.ts:9-50 reads
LOCAL_STUDIO_BROWSER_SESSION_ID and includes sessionId in action bodies.
services/agent-runtime/src/http/browser-handlers.ts:67-73 deliberately strips
that field because the host is global.
browser-handlers.ts:47 stores one process-global lastFallbackUrl.
services/agent-runtime/src/browser-host/browser-host.ts:75-77 owns one
activeId and a global page map; all verbs omit a session key.
- Frame, state, input, and viewport endpoints also operate on the global page.
services/agent-runtime/src/pi-runtime-helpers.ts:318 already injects the
browser session ID into Pi runtime options.
Steps to reproduce
- Start two agent sessions, A and B, and enable the embedded browser in both.
- From A, navigate to a loopback fixture that sets recognizable page and
storage state.
- From B, request browser state or a screenshot, then navigate or send input.
- Return to A and observe that both sessions addressed and mutated the same
global page; the fallback URL is global as well.
Expected: each session has an isolated page, storage context, fallback state,
input stream, and cleanup lifecycle. Actual: all browser endpoints discard the
provided session identity and operate on one process-global target.
Verification commands
| Purpose |
Command |
Expected on success |
| Runtime tests |
npm --prefix services/agent-runtime test -- browser-session |
all isolation/capacity cases pass |
| Frontend tests |
npm --prefix frontend run test |
session transport cases pass |
| Repository gate |
npm run check |
exit 0 |
| Integration |
npm run test:integration |
browser regressions pass |
| Desktop build |
npm --prefix frontend run desktop:dist |
production bundle built |
Scope
In scope:
services/agent-runtime/src/browser-host/browser-session.ts (create)
services/agent-runtime/src/browser-host/browser-host.ts
services/agent-runtime/src/browser-host/chrome.ts and cdp.ts as needed for
browser contexts/target creation
services/agent-runtime/src/http/browser-handlers.ts
frontend/desktop/resources/pi-extensions/browser.ts
frontend/desktop/resources/pi-extensions/sitegeist-browser.ts
frontend/src/features/agent/ui/agent-browser-panel.tsx
frontend/src/features/agent/ui/agent-browser-effects.ts
frontend/src/features/agent/ui/agent-browser-screencast.tsx
services/agent-runtime/src/pi-runtime-helpers.ts and focused tests only if
the existing injected key needs canonicalization
- Focused runtime/frontend/regression tests
Out of scope:
- Separate Chromium OS processes per session; one managed process may host
isolated browser contexts.
- Persisting cookies/history after a session is released.
- Cross-device browser synchronization.
- Stateless
/fetch and /localhosts, which remain independent of page state.
Solution design
Define a canonical session key schema (1-128 characters, restricted stable
ASCII) and transport it in one header for verbs, frame, state, input, and
viewport. Missing/malformed keys return 400 before touching Chromium. BrowserHost
maps each key to a dedicated incognito browser context, one page, frame/poll
state, fallback URL, and last-access timestamp. Creation is coalesced per key.
Use LOCAL_STUDIO_BROWSER_MAX_SESSIONS with default 8 and accepted integer
range 1..32, plus LOCAL_STUDIO_BROWSER_SESSION_IDLE_MS with default 900000
(15 minutes) and accepted range 60000..86400000. Unset values use defaults;
invalid explicit values fail startup through Effect Schema rather than clamping.
Evict only idle sessions and fail closed when all capacity is active.
releaseSession closes page/context and removes all state. UI uses the same
focused session ID already passed to Pi; no focused session means the live
browser is disabled, not mapped to a shared default.
Implementation plan
Step 1: Define and enforce one session-key contract
Create an Effect schema/parser and a stable header name shared by server,
extension, and UI. Parse before JSON/body dispatch. Remove the body-field strip
and global fallback URL.
Verify: every stateful endpoint returns 400 for missing, empty, oversized,
Unicode/control, and malformed keys; valid boundary keys pass.
Step 2: Create one Chromium context and page per key
Use browser-level CDP Target.createBrowserContext and target creation within
that context. Store all page/fallback/poll/viewport state in a session record.
Serialize duplicate creation and teardown for the same key.
Verify: two simultaneous first calls for one key create one context; two
different keys have different context/target IDs and independent cookies/storage.
Step 3: Thread the key through every stateful operation
Require the parsed key for navigate/get URL/text/HTML/screenshot/click/fill/
scroll/history/reload, frame, state, input, and viewport. Keep /fetch and
/localhosts stateless. Never accept a page ID belonging to another context.
Verify: a test navigates A and B to distinct fixtures, sends input to A, and
asserts B's URL, DOM, frame, cookies, history, and input log remain unchanged.
Step 4: Wire visible and Pi callers to the same key
Set the header in both Pi extensions and every screencast/UI fetch using the
focused agent session ID. Remove any fallback to an empty/default key. Ensure
session changes unsubscribe old polling before starting the new one.
Verify: frontend tests assert every stateful request carries the focused key
and switching tabs stops old frame/input traffic.
Step 5: Add bounded lifecycle management
Track last access, use an Effect fiber for TTL cleanup, enforce the default
maximum of 8 contexts and 15-minute idle TTL, validate the two configuration
bounds above, and expose idempotent releaseSession. Do not evict a session with
in-flight navigation/input/frame work. Close all contexts on browser-host
shutdown.
Verify: fake-clock tests cover idle eviction, active protection, LRU capacity,
all-active refusal, release idempotency, context cleanup, both defaults, minimum
and maximum accepted values, and rejection immediately outside each range.
Step 6: Run gates and reinstall desktop
Run runtime/frontend/full tests, build desktop:dist, replace the canonical app
using AGENTS.md, relaunch, and query /api/desktop-health.
Verify: npm --prefix services/agent-runtime run check && npm --prefix frontend run check:quality && npm --prefix frontend run test && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist → all exit 0; installed health is HTTP 200 with ok: true.
Test plan
- Header schema boundaries and conflicting body/header values.
- Two sessions: URL, DOM/text, screenshot/frame, history, viewport, input,
cookies/localStorage, fallback URL, and navigation errors are isolated.
- Concurrent create/release and request during release.
- Missing session never creates a page/context.
- TTL, hard capacity, LRU idle eviction, all-active refusal, and shutdown cleanup.
- Pi and visible pane for one session share state; another pane cannot observe it.
Acceptance criteria
Risks and stop conditions
Stop and report if:
- Managed Chromium cannot create isolated browser contexts through a supported
CDP endpoint.
- audit proposal 041 has not removed the separate desktop webview.
- A caller lacks a stable agent session ID.
- Isolation would still share cookies/service workers/storage between contexts.
- Desktop reinstall or health verification cannot complete.
Maintenance considerations
The session key is an isolation key, not authentication; existing route access
controls still apply. Reviewers should inspect every endpoint, including frame
and input. audit proposal 043 should call releaseSession when a Pi runtime is disposed.
Generated from Local Studio 2.0 main-branch audit proposal 042.
Audit metadata
07be5be7ce69bea0c3118744ab90d148b010fce0(origin/mainon 2026-07-17)Dependencies
Description and impact
Pi already sends a browser session ID, but the server discards it and routes all
users/tabs to one global page and fallback URL. Concurrent sessions can navigate,
read, click, type into, screenshot, or inherit cookies from each other's page.
Session-scoped pages and Chromium contexts are required for correctness and
privacy once the visible and agent browser share one host.
Current state
frontend/desktop/resources/pi-extensions/browser.ts:9-50readsLOCAL_STUDIO_BROWSER_SESSION_IDand includessessionIdin action bodies.services/agent-runtime/src/http/browser-handlers.ts:67-73deliberately stripsthat field because the host is global.
browser-handlers.ts:47stores one process-globallastFallbackUrl.services/agent-runtime/src/browser-host/browser-host.ts:75-77owns oneactiveIdand a global page map; all verbs omit a session key.services/agent-runtime/src/pi-runtime-helpers.ts:318already injects thebrowser session ID into Pi runtime options.
Steps to reproduce
storage state.
global page; the fallback URL is global as well.
Expected: each session has an isolated page, storage context, fallback state,
input stream, and cleanup lifecycle. Actual: all browser endpoints discard the
provided session identity and operate on one process-global target.
Verification commands
npm --prefix services/agent-runtime test -- browser-sessionnpm --prefix frontend run testnpm run checknpm run test:integrationnpm --prefix frontend run desktop:distScope
In scope:
services/agent-runtime/src/browser-host/browser-session.ts(create)services/agent-runtime/src/browser-host/browser-host.tsservices/agent-runtime/src/browser-host/chrome.tsandcdp.tsas needed forbrowser contexts/target creation
services/agent-runtime/src/http/browser-handlers.tsfrontend/desktop/resources/pi-extensions/browser.tsfrontend/desktop/resources/pi-extensions/sitegeist-browser.tsfrontend/src/features/agent/ui/agent-browser-panel.tsxfrontend/src/features/agent/ui/agent-browser-effects.tsfrontend/src/features/agent/ui/agent-browser-screencast.tsxservices/agent-runtime/src/pi-runtime-helpers.tsand focused tests only ifthe existing injected key needs canonicalization
Out of scope:
isolated browser contexts.
/fetchand/localhosts, which remain independent of page state.Solution design
Define a canonical session key schema (1-128 characters, restricted stable
ASCII) and transport it in one header for verbs, frame, state, input, and
viewport. Missing/malformed keys return 400 before touching Chromium. BrowserHost
maps each key to a dedicated incognito browser context, one page, frame/poll
state, fallback URL, and last-access timestamp. Creation is coalesced per key.
Use
LOCAL_STUDIO_BROWSER_MAX_SESSIONSwith default8and accepted integerrange
1..32, plusLOCAL_STUDIO_BROWSER_SESSION_IDLE_MSwith default900000(15 minutes) and accepted range
60000..86400000. Unset values use defaults;invalid explicit values fail startup through Effect Schema rather than clamping.
Evict only idle sessions and fail closed when all capacity is active.
releaseSessioncloses page/context and removes all state. UI uses the samefocused session ID already passed to Pi; no focused session means the live
browser is disabled, not mapped to a shared default.
Implementation plan
Step 1: Define and enforce one session-key contract
Create an Effect schema/parser and a stable header name shared by server,
extension, and UI. Parse before JSON/body dispatch. Remove the body-field strip
and global fallback URL.
Verify: every stateful endpoint returns 400 for missing, empty, oversized,
Unicode/control, and malformed keys; valid boundary keys pass.
Step 2: Create one Chromium context and page per key
Use browser-level CDP
Target.createBrowserContextand target creation withinthat context. Store all page/fallback/poll/viewport state in a session record.
Serialize duplicate creation and teardown for the same key.
Verify: two simultaneous first calls for one key create one context; two
different keys have different context/target IDs and independent cookies/storage.
Step 3: Thread the key through every stateful operation
Require the parsed key for navigate/get URL/text/HTML/screenshot/click/fill/
scroll/history/reload, frame, state, input, and viewport. Keep
/fetchand/localhostsstateless. Never accept a page ID belonging to another context.Verify: a test navigates A and B to distinct fixtures, sends input to A, and
asserts B's URL, DOM, frame, cookies, history, and input log remain unchanged.
Step 4: Wire visible and Pi callers to the same key
Set the header in both Pi extensions and every screencast/UI fetch using the
focused agent session ID. Remove any fallback to an empty/default key. Ensure
session changes unsubscribe old polling before starting the new one.
Verify: frontend tests assert every stateful request carries the focused key
and switching tabs stops old frame/input traffic.
Step 5: Add bounded lifecycle management
Track last access, use an Effect fiber for TTL cleanup, enforce the default
maximum of 8 contexts and 15-minute idle TTL, validate the two configuration
bounds above, and expose idempotent
releaseSession. Do not evict a session within-flight navigation/input/frame work. Close all contexts on browser-host
shutdown.
Verify: fake-clock tests cover idle eviction, active protection, LRU capacity,
all-active refusal, release idempotency, context cleanup, both defaults, minimum
and maximum accepted values, and rejection immediately outside each range.
Step 6: Run gates and reinstall desktop
Run runtime/frontend/full tests, build
desktop:dist, replace the canonical appusing
AGENTS.md, relaunch, and query/api/desktop-health.Verify:
npm --prefix services/agent-runtime run check && npm --prefix frontend run check:quality && npm --prefix frontend run test && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist→ all exit 0; installed health is HTTP 200 withok: true.Test plan
cookies/localStorage, fallback URL, and navigation errors are isolated.
Acceptance criteria
explicit configuration is schema-validated within documented bounds.
Risks and stop conditions
Stop and report if:
CDP endpoint.
Maintenance considerations
The session key is an isolation key, not authentication; existing route access
controls still apply. Reviewers should inspect every endpoint, including frame
and input. audit proposal 043 should call
releaseSessionwhen a Pi runtime is disposed.Generated from Local Studio 2.0 main-branch audit proposal 042.