diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index d75aca1..c70ae6a 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -169,6 +169,9 @@ export default defineBackground(() => { if (!sessions.findByWindowId(tab.windowId)) return; void pushOverlayStateForTab(tab.id, tab.windowId); }); + chrome.tabs.onRemoved.addListener((tabId) => { + sessions.forgetAgentCreatedTab(tabId); + }); // Re-sync the storage.session flag on SW startup so a previous SW's // stale `true` does not keep waking us on every page load until the // first mutation (review M4/M5 round 3 m-R3-1). @@ -215,6 +218,9 @@ export default defineBackground(() => { recording: recordDeps, onSessionsChanged: onOverlaySessionStateChanged, onBrowserControlResumed, + onAgentTabClaimed: (tabId, windowId) => { + void pushOverlayStateForTab(tabId, windowId); + }, approveBorrow: (ctx) => requestBorrowConfirmation(ctx.tabId, { ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}), @@ -341,12 +347,9 @@ export default defineBackground(() => { if (!msg || typeof msg !== "object" || !("kind" in msg)) return false; if (msg.kind === OVERLAY_MSG_WHO_AM_I) { - const tabId = sender.tab?.id; const windowId = sender.tab?.windowId; const ctx = typeof windowId === "number" ? sessions.findByWindowId(windowId) : null; - const sessionId = - ctx && typeof tabId === "number" && isAgentControlledTab(ctx, tabId) ? ctx.sessionId : null; - sendResponse({ sessionId }); + sendResponse({ sessionId: ctx?.sessionId ?? null }); return false; } diff --git a/apps/extension/src/session-manager/__tests__/manager.test.ts b/apps/extension/src/session-manager/__tests__/manager.test.ts index fbbbf7e..ab231af 100644 --- a/apps/extension/src/session-manager/__tests__/manager.test.ts +++ b/apps/extension/src/session-manager/__tests__/manager.test.ts @@ -175,6 +175,15 @@ describe("SessionManager", () => { reservation.commit({ tabId: 42, originalWindowId: 7, originalIndex: 3 }); expect(isAgentControlledTab(ctx, 42)).toBe(true); }); + + it("forgets an agent-created tab after Chrome removes it", async () => { + const sm = new SessionManager({ agentWindow: fakeAgentWindow() }); + const ctx = await sm.start("aa11"); + ctx.agentCreatedTabs.add(42); + sm.forgetAgentCreatedTab(42); + expect(ctx.agentCreatedTabs.has(42)).toBe(false); + expect(isAgentControlledTab(ctx, 0)).toBe(true); + }); }); describe("findBorrowingSession", () => { diff --git a/apps/extension/src/session-manager/manager.ts b/apps/extension/src/session-manager/manager.ts index d721c41..4c0df05 100644 --- a/apps/extension/src/session-manager/manager.ts +++ b/apps/extension/src/session-manager/manager.ts @@ -117,6 +117,13 @@ export class SessionManager { return Array.from(this.sessions.values()); } + /** Remove a closed tab from agent-created ownership tracking. */ + forgetAgentCreatedTab(tabId: number): void { + for (const ctx of this.sessions.values()) { + ctx.agentCreatedTabs.delete(tabId); + } + } + /** * Look up whether `tabId` is currently borrowed by some *other* * session than the one calling. Used by M8 `tab_borrow` to refuse diff --git a/apps/extension/src/tools/__tests__/dispatcher.test.ts b/apps/extension/src/tools/__tests__/dispatcher.test.ts index 6d7dceb..8509187 100644 --- a/apps/extension/src/tools/__tests__/dispatcher.test.ts +++ b/apps/extension/src/tools/__tests__/dispatcher.test.ts @@ -458,6 +458,16 @@ describe("ToolDispatcher", () => { }); it("invokes onBrowserControlResumed for browser-control tools but not passive reads", async () => { + vi.stubGlobal("chrome", { + tabs: { + create: vi.fn(async (props: chrome.tabs.CreateProperties) => ({ + id: 7, + windowId: props.windowId, + url: props.url, + active: props.active, + })), + }, + }); const { transport, sent, deliver } = fakeTransport(); const sessions = new SessionManager({ agentWindow: { @@ -467,7 +477,13 @@ describe("ToolDispatcher", () => { }, }); const onBrowserControlResumed = vi.fn(); - const dispatcher = new ToolDispatcher({ transport, sessions, onBrowserControlResumed }); + const onAgentTabClaimed = vi.fn(); + const dispatcher = new ToolDispatcher({ + transport, + sessions, + onBrowserControlResumed, + onAgentTabClaimed, + }); dispatcher.start(); deliver(makeRequest("tool.session_start", { session_id: "aa11" })); @@ -484,6 +500,7 @@ describe("ToolDispatcher", () => { }); await flushMicrotasks(); expect(onBrowserControlResumed).toHaveBeenCalledWith("aa11"); + expect(onAgentTabClaimed).toHaveBeenCalledWith(7, 1); }); it("reasserts remembered hover before follow-up work and releases only after actions", async () => { diff --git a/apps/extension/src/tools/__tests__/emulate.test.ts b/apps/extension/src/tools/__tests__/emulate.test.ts index 3782fdc..d8e27af 100644 --- a/apps/extension/src/tools/__tests__/emulate.test.ts +++ b/apps/extension/src/tools/__tests__/emulate.test.ts @@ -20,7 +20,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 7), + ensureActiveTab: vi.fn(async () => 1), }; } @@ -197,7 +197,6 @@ describe("handleEmulate", () => { it("targets an explicit tab_id", async () => { const sm = await makeManager(); - sm.get("aa11")?.agentCreatedTabs.add(9); const deps = makeDeps(); const result = await handleEmulate( sm, @@ -277,7 +276,6 @@ describe("handleEmulate", () => { it("keeps emulation state per tab", async () => { const sm = await makeManager(); - sm.get("aa11")?.agentCreatedTabs.add(9); const deps = makeDeps(); await handleEmulate(sm, { session_id: "aa11", overrides: fullOverrides }, deps); await handleEmulate( diff --git a/apps/extension/src/tools/__tests__/evaluate.test.ts b/apps/extension/src/tools/__tests__/evaluate.test.ts index d9c352d..0065499 100644 --- a/apps/extension/src/tools/__tests__/evaluate.test.ts +++ b/apps/extension/src/tools/__tests__/evaluate.test.ts @@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 4), + ensureActiveTab: vi.fn(async () => 1), }; } diff --git a/apps/extension/src/tools/__tests__/human-loop.test.ts b/apps/extension/src/tools/__tests__/human-loop.test.ts index f5a0f18..a500735 100644 --- a/apps/extension/src/tools/__tests__/human-loop.test.ts +++ b/apps/extension/src/tools/__tests__/human-loop.test.ts @@ -52,16 +52,10 @@ function installHelpLifecycleChrome() { function fakeManager(sessionId: string, agentWindowId: number, tabId: number) { const refStore = new RefStore(); - const session = { - sessionId, - agentWindowId, - refStore, - borrowedTabs: new Map(), - agentCreatedTabs: new Set([tabId]), - }; const mgr = { - get: (id: string) => (id === sessionId ? session : null), - findByWindowId: (wid: number) => (wid === agentWindowId ? session : null), + get: (id: string) => + id === sessionId ? { sessionId, agentWindowId, refStore, borrowedTabs: new Map() } : null, + findByWindowId: (wid: number) => (wid === agentWindowId ? { sessionId } : null), } as unknown as SessionManager; return mgr; } @@ -363,7 +357,6 @@ describe("handleRequestHelp", () => { agentWindowId: 99, refStore, borrowedTabs: new Map(), - agentCreatedTabs: new Set([5]), } : null, findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null), @@ -420,7 +413,6 @@ describe("handleRequestHelp", () => { }), }, borrowedTabs: new Map(), - agentCreatedTabs: new Set([5]), } : null, findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null), @@ -506,7 +498,6 @@ describe("handleRequestHelp", () => { agentWindowId: 99, refStore, borrowedTabs: new Map(), - agentCreatedTabs: new Set([5]), } : null, findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null), diff --git a/apps/extension/src/tools/__tests__/interaction.test.ts b/apps/extension/src/tools/__tests__/interaction.test.ts index bf4313b..45ab74e 100644 --- a/apps/extension/src/tools/__tests__/interaction.test.ts +++ b/apps/extension/src/tools/__tests__/interaction.test.ts @@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 4), + ensureActiveTab: vi.fn(async () => 1), }; } @@ -113,7 +113,6 @@ describe("handleClick", () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.refStore.set("e3", 1234, { tabId: 4 }); - ctx.agentCreatedTabs.add(5); const fake = makeFakeCdp({}); const res = await handleClick( sm, diff --git a/apps/extension/src/tools/__tests__/navigation.test.ts b/apps/extension/src/tools/__tests__/navigation.test.ts index 5e08f55..921771e 100644 --- a/apps/extension/src/tools/__tests__/navigation.test.ts +++ b/apps/extension/src/tools/__tests__/navigation.test.ts @@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 4), + ensureActiveTab: vi.fn(async () => 1), }; } diff --git a/apps/extension/src/tools/__tests__/observation.test.ts b/apps/extension/src/tools/__tests__/observation.test.ts index d5a5855..0763262 100644 --- a/apps/extension/src/tools/__tests__/observation.test.ts +++ b/apps/extension/src/tools/__tests__/observation.test.ts @@ -28,7 +28,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 4), + ensureActiveTab: vi.fn(async () => 1), }; } diff --git a/apps/extension/src/tools/__tests__/record-steps.test.ts b/apps/extension/src/tools/__tests__/record-steps.test.ts index e37a793..f933b49 100644 --- a/apps/extension/src/tools/__tests__/record-steps.test.ts +++ b/apps/extension/src/tools/__tests__/record-steps.test.ts @@ -84,7 +84,6 @@ function fakeManager() { agentWindowId: AGENT_WINDOW_ID, refStore: { resolve: () => null, replace: () => {} }, borrowedTabs: new Map(), - agentCreatedTabs: new Set([TAB_ID]), } : null, findByWindowId: (windowId: number) => diff --git a/apps/extension/src/tools/__tests__/session.test.ts b/apps/extension/src/tools/__tests__/session.test.ts index 4853939..d4cd548 100644 --- a/apps/extension/src/tools/__tests__/session.test.ts +++ b/apps/extension/src/tools/__tests__/session.test.ts @@ -301,8 +301,6 @@ describe("handleSessionStop window release (issue #57)", () => { const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); ctx.agentCreatedTabs.add(12); - // home tab id from ensureActiveTab override - ctx.agentCreatedTabs.add(10); // home tab // user tab 99 (created via Chrome UI, NOT in agentCreatedTabs) const state: FakeState = { tabs: new Map([ @@ -340,7 +338,6 @@ describe("handleSessionStop window release (issue #57)", () => { const sm = new SessionManager({ agentWindow: aw }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); - ctx.agentCreatedTabs.add(10); // home tab const state: FakeState = { tabs: new Map([ [10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab], @@ -369,7 +366,6 @@ describe("handleSessionStop window release (issue #57)", () => { const sm = new SessionManager({ agentWindow: aw }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); // no longer in state → remove throws - ctx.agentCreatedTabs.add(10); // home tab const state: FakeState = { tabs: new Map([[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab]]), windowsClosed: new Set(), @@ -399,7 +395,6 @@ describe("handleSessionStop window release (issue #57)", () => { const sm = new SessionManager({ agentWindow: aw }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); - ctx.agentCreatedTabs.add(10); // home tab const state: FakeState = { tabs: new Map([ [10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab], @@ -427,7 +422,6 @@ describe("handleSessionStop window release (issue #57)", () => { const sm = new SessionManager({ agentWindow: aw }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); - ctx.agentCreatedTabs.add(10); // home tab const state: FakeState = { tabs: new Map([[11, { id: 11, windowId: agentWindowId } as chrome.tabs.Tab]]), windowsClosed: new Set(), @@ -473,7 +467,6 @@ describe("handleSessionStop window release (issue #57)", () => { const sm = new SessionManager({ agentWindow: aw }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(11); // Step 4 remove() will throw for this tab - ctx.agentCreatedTabs.add(10); // home tab const state: FakeState = { tabs: new Map([ [10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab], diff --git a/apps/extension/src/tools/__tests__/tabs.test.ts b/apps/extension/src/tools/__tests__/tabs.test.ts index 4a120a1..ffc6444 100644 --- a/apps/extension/src/tools/__tests__/tabs.test.ts +++ b/apps/extension/src/tools/__tests__/tabs.test.ts @@ -60,24 +60,19 @@ describe("handleTabList", () => { expect(res.tabs.map((t) => t.scope).sort()).toEqual(["agent", "user"]); }); - it("scope=user includes unclaimed tabs inside the Agent Window", async () => { + it("scope=user excludes the requesting session's Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const tabs = fakeChromeTabsApi([ { windowId: 100, url: "chrome://newtab/", title: "Agent New Tab", active: true }, - { windowId: 100, url: "https://user.example/", title: "Free user tab", active: false }, { windowId: 200, url: "https://example.com", title: "Example", active: true }, { windowId: 200, url: "https://github.com", title: "GitHub", active: false }, ]); const res = await handleTabList(sm, { session_id: "aa11", scope: "user" }, tabs); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); - expect(res.tabs).toHaveLength(3); + expect(res.tabs).toHaveLength(2); expect(res.tabs.every((t) => t.scope === "user")).toBe(true); - expect(res.tabs.map((t) => t.url)).toEqual([ - "https://user.example/", - "https://example.com", - "https://github.com", - ]); + expect(res.tabs.map((t) => t.url)).toEqual(["https://example.com", "https://github.com"]); }); it("scope=agent returns only the requesting session's Agent Window tabs", async () => { @@ -302,8 +297,7 @@ describe("handleTabCreate", () => { describe("handleTabClose", () => { it("removes a tab inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); - const ctx = await sm.start("aa11"); - ctx.agentCreatedTabs.add(5); + await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, @@ -316,23 +310,6 @@ describe("handleTabClose", () => { expect(spies.remove).toHaveBeenCalledWith(5); }); - it("refuses to close a free tab in the Agent Window", async () => { - const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); - await sm.start("aa11"); - const state: FakeTabState = { - tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]), - nextTabId: 50, - windowsClosed: new Set(), - }; - const { api, spies } = makeTabMutationApi(state); - const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 5 }, { tabs: api }); - expect(res).toMatchObject({ - code: "permission_denied", - data: { reason: "agent_window_scope" }, - }); - expect(spies.remove).not.toHaveBeenCalled(); - }); - it("rejects closing a borrowed tab — caller must tab_return first", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); @@ -383,8 +360,7 @@ describe("handleTabClose", () => { describe("handleTabSelect", () => { it("activates a tab inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); - const ctx = await sm.start("aa11"); - ctx.agentCreatedTabs.add(3); + await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[3, { id: 3, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, diff --git a/apps/extension/src/tools/__tests__/waits.test.ts b/apps/extension/src/tools/__tests__/waits.test.ts index 81346be..ce0f3aa 100644 --- a/apps/extension/src/tools/__tests__/waits.test.ts +++ b/apps/extension/src/tools/__tests__/waits.test.ts @@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) { return id; }), remove: vi.fn(async () => {}), - ensureActiveTab: vi.fn(async () => 4), + ensureActiveTab: vi.fn(async () => 1), }; } diff --git a/apps/extension/src/tools/dispatcher.ts b/apps/extension/src/tools/dispatcher.ts index 8590478..2752791 100644 --- a/apps/extension/src/tools/dispatcher.ts +++ b/apps/extension/src/tools/dispatcher.ts @@ -115,6 +115,8 @@ export interface DispatcherDeps { onSessionsChanged?: () => void; /** Invoked before a tool that dispatches page input or mutates browser state is forwarded. */ onBrowserControlResumed?: (sessionId: string) => void; + /** Invoked after a tab is explicitly claimed so its overlay can be refreshed immediately. */ + onAgentTabClaimed?: (tabId: number, windowId: number) => void; /** User approval for `tool.tab_borrow` (overlay in content script). */ approveBorrow?: BorrowConfirmationApprover; /** i18n notification copy for `tool.request_help` (resolved per-call). */ @@ -144,6 +146,7 @@ export class ToolDispatcher { private readonly recording?: RecordRuntimeDeps; private readonly onSessionsChanged?: () => void; private readonly onBrowserControlResumed?: (sessionId: string) => void; + private readonly onAgentTabClaimed?: (tabId: number, windowId: number) => void; private readonly approveBorrow?: BorrowConfirmationApprover; private readonly helpNotificationCopy?: () => { title: string; body: string }; private subscription: { dispose(): void } | null = null; @@ -164,6 +167,7 @@ export class ToolDispatcher { this.recording = deps.recording; this.onSessionsChanged = deps.onSessionsChanged; this.onBrowserControlResumed = deps.onBrowserControlResumed; + this.onAgentTabClaimed = deps.onAgentTabClaimed; this.approveBorrow = deps.approveBorrow; this.helpNotificationCopy = deps.helpNotificationCopy; } @@ -309,8 +313,15 @@ export class ToolDispatcher { } case "tool.tab_list": return handleTabList(this.sessions, req.params as TabListParams, chromeTabsApi, signal); - case "tool.tab_create": - return handleTabCreate(this.sessions, req.params as TabCreateParams, { signal }); + case "tool.tab_create": { + const result = await handleTabCreate(this.sessions, req.params as TabCreateParams, { + signal, + }); + if (!isRpcError(result)) { + this.onAgentTabClaimed?.(result.tab_id, result.window_id); + } + return result; + } case "tool.tab_close": return this.withHoverReleaseForRequest( req.params as TabCloseParams, @@ -319,11 +330,16 @@ export class ToolDispatcher { ); case "tool.tab_select": return handleTabSelect(this.sessions, req.params as TabSelectParams, { signal }); - case "tool.tab_borrow": - return handleTabBorrow(this.sessions, req.params as TabBorrowParams, { + case "tool.tab_borrow": { + const result = await handleTabBorrow(this.sessions, req.params as TabBorrowParams, { signal, approveBorrow: this.approveBorrow, }); + if (!isRpcError(result)) { + this.onAgentTabClaimed?.(result.tab_id, result.agent_window_id); + } + return result; + } case "tool.tab_return": return handleTabReturn(this.sessions, req.params as TabReturnParams, { signal }); case "tool.window_resize": diff --git a/apps/extension/src/tools/shared.ts b/apps/extension/src/tools/shared.ts index 829ba0f..3b2cb79 100644 --- a/apps/extension/src/tools/shared.ts +++ b/apps/extension/src/tools/shared.ts @@ -8,11 +8,7 @@ import type { CdpDebuggee, DialogCursor } from "@/browser-driver/chromium-cdp"; import type { CdpFrameGraph, CdpTarget } from "@/browser-driver/frame-graph"; -import { - isAgentControlledTab, - type SessionContext, - type SessionManager, -} from "@/session-manager/manager"; +import type { SessionContext, SessionManager } from "@/session-manager/manager"; import { normaliseRef } from "@/session-manager/ref-store"; import type { ConsoleResult, JavaScriptDialogInfo, RpcError } from "@/transport/types"; import { rpcError } from "./errors"; @@ -327,22 +323,23 @@ export async function resolveCdpAccessibleTargetTab( } /** - * Sandbox guard: M7 write tools (click / fill / press / navigate*) must - * refuse tabs the session has not explicitly claimed. A controlled tab must - * also remain inside the session's Agent Window. + * Sandbox guard: M7 write tools (click / fill / press / navigate*) + * MUST refuse to touch a tab outside the session's Agent Window + * (§6 — borrowing brings the tab into the Agent Window first). * - * Returns an `RpcError` for free tabs and tabs outside the Agent Window. + * Returns an `RpcError` when the resolved target sits in a user window; + * `null` on success. */ export function enforceAgentWindow( ctx: SessionContext, target: { tabId: number; windowId: number }, toolName: string, ): RpcError | null { - if (target.windowId !== ctx.agentWindowId || !isAgentControlledTab(ctx, target.tabId)) { + if (target.windowId !== ctx.agentWindowId) { return rpcError( "permission_denied", "agent_window_scope", - `${toolName} can only act on tabs claimed by this session inside the Agent Window (tab ${target.tabId} is free or outside window ${ctx.agentWindowId}; borrow it first)`, + `${toolName} can only act on tabs inside the Agent Window (tab ${target.tabId} is in window ${target.windowId}; borrow it first)`, ); } return null; diff --git a/apps/extension/src/tools/tabs.ts b/apps/extension/src/tools/tabs.ts index 1192fa6..9057354 100644 --- a/apps/extension/src/tools/tabs.ts +++ b/apps/extension/src/tools/tabs.ts @@ -7,11 +7,7 @@ import { OVERLAY_AGENT_OVERLAY_RESET, type OverlayAgentOverlayResetMessage, } from "@/lib/overlay-bridge"; -import { - isAgentControlledTab, - type SessionContext, - type SessionManager, -} from "@/session-manager/manager"; +import type { SessionContext, SessionManager } from "@/session-manager/manager"; import type { RpcError } from "@/transport/types"; import { rpcError } from "./errors"; import { isRpcError, lookupSession } from "./shared"; @@ -29,9 +25,11 @@ export interface TabInfo { window_id?: number; active?: boolean; /** - * Ownership relative to the requesting session. Explicitly created or - * borrowed tabs in this session's Agent Window are `agent`; every other - * visible tab is `user`. Other sessions' Agent Windows are filtered out. + * Where the tab sits relative to the requesting session: tabs in any + * window other than an Agent Window are `user`; tabs in this + * session's own Agent Window are `agent`. Tabs in *other* sessions' + * Agent Windows are filtered out entirely (cross-session isolation, + * design §6). */ scope?: "user" | "agent"; } @@ -261,8 +259,7 @@ export async function handleTabList( if (typeof t.id !== "number") continue; const winId = typeof t.windowId === "number" ? t.windowId : -1; if (otherAgentWindowIds.has(winId)) continue; - const tabScope: "user" | "agent" = - winId === myAgentWindowId && isAgentControlledTab(ctx, t.id) ? "agent" : "user"; + const tabScope: "user" | "agent" = winId === myAgentWindowId ? "agent" : "user"; if (scope === "user" && tabScope !== "user") continue; if (scope === "agent" && tabScope !== "agent") continue; tabs.push({ @@ -484,13 +481,13 @@ async function authoriseAgentTab( `${toolName}: tab ${tabId} is borrowed by session ${otherBorrower}`, ); } - if (tab.windowId === ctx.agentWindowId && isAgentControlledTab(ctx, tab.id)) { + if (tab.windowId === ctx.agentWindowId) { return tab; } return rpcError( "permission_denied", "agent_window_scope", - `${toolName}: tab ${tabId} is not claimed by this session in Agent Window ${ctx.agentWindowId}`, + `${toolName}: tab ${tabId} is not in Agent Window ${ctx.agentWindowId}`, ); }