From 8fa3d7b06dce6596d6cceb74cf9c7f0d6b356546 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 4 Apr 2026 23:18:23 -0700 Subject: [PATCH] feat: tab isolation for multi-agent browser access Add per-tab ownership tracking to BrowserManager. Scoped agents must create their own tab via newtab before writing. Unowned tabs (pre-existing, user-opened) are root-only for writes. Read access always allowed. Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/browser-manager.ts | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index ef476248..a417f407 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -46,6 +46,10 @@ export class BrowserManager { /** Server port — set after server starts, used by cookie-import-browser command */ public serverPort: number = 0; + // ─── Tab Ownership (multi-agent isolation) ────────────── + // Maps tabId → clientId. Unowned tabs (not in this map) are root-only for writes. + private tabOwnership: Map = new Map(); + // ─── Ref Map (snapshot → @e1, @e2, @c1, @c2, ...) ──────── private refMap: Map = new Map(); @@ -506,7 +510,7 @@ export class BrowserManager { } // ─── Tab Management ──────────────────────────────────────── - async newTab(url?: string): Promise { + async newTab(url?: string, clientId?: string): Promise { if (!this.context) throw new Error('Browser not launched'); // Validate URL before allocating page to avoid zombie tabs on rejection @@ -519,6 +523,11 @@ export class BrowserManager { this.pages.set(id, page); this.activeTabId = id; + // Record tab ownership for multi-agent isolation + if (clientId) { + this.tabOwnership.set(id, clientId); + } + // Wire up console/network/dialog capture this.wirePageEvents(page); @@ -536,6 +545,7 @@ export class BrowserManager { await page.close(); this.pages.delete(tabId); + this.tabOwnership.delete(tabId); // Switch to another tab if we closed the active one if (tabId === this.activeTabId) { @@ -611,6 +621,32 @@ export class BrowserManager { return this.pages.size; } + // ─── Tab Ownership (multi-agent isolation) ────────────── + + /** Get the owner of a tab, or null if unowned (root-only for writes). */ + getTabOwner(tabId: number): string | null { + return this.tabOwnership.get(tabId) || null; + } + + /** + * Check if a client can access a tab. + * Read access is always allowed. Write access requires ownership. + * Unowned tabs are root-only for writes. + */ + checkTabAccess(tabId: number, clientId: string, isWrite: boolean): boolean { + if (clientId === 'root') return true; + if (!isWrite) return true; + const owner = this.tabOwnership.get(tabId); + if (!owner) return false; // unowned = root-only for writes + return owner === clientId; + } + + /** Transfer tab ownership to a different client. */ + transferTab(tabId: number, toClientId: string): void { + if (!this.pages.has(tabId)) throw new Error(`Tab ${tabId} not found`); + this.tabOwnership.set(tabId, toClientId); + } + async getTabListWithTitles(): Promise> { const tabs: Array<{ id: number; url: string; title: string; active: boolean }> = []; for (const [id, page] of this.pages) {