mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(extension): preserve existing tab scope semantics
This commit is contained in:
@@ -169,6 +169,9 @@ export default defineBackground(() => {
|
|||||||
if (!sessions.findByWindowId(tab.windowId)) return;
|
if (!sessions.findByWindowId(tab.windowId)) return;
|
||||||
void pushOverlayStateForTab(tab.id, tab.windowId);
|
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
|
// 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
|
// stale `true` does not keep waking us on every page load until the
|
||||||
// first mutation (review M4/M5 round 3 m-R3-1).
|
// first mutation (review M4/M5 round 3 m-R3-1).
|
||||||
@@ -215,6 +218,9 @@ export default defineBackground(() => {
|
|||||||
recording: recordDeps,
|
recording: recordDeps,
|
||||||
onSessionsChanged: onOverlaySessionStateChanged,
|
onSessionsChanged: onOverlaySessionStateChanged,
|
||||||
onBrowserControlResumed,
|
onBrowserControlResumed,
|
||||||
|
onAgentTabClaimed: (tabId, windowId) => {
|
||||||
|
void pushOverlayStateForTab(tabId, windowId);
|
||||||
|
},
|
||||||
approveBorrow: (ctx) =>
|
approveBorrow: (ctx) =>
|
||||||
requestBorrowConfirmation(ctx.tabId, {
|
requestBorrowConfirmation(ctx.tabId, {
|
||||||
...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
|
...(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 || typeof msg !== "object" || !("kind" in msg)) return false;
|
||||||
|
|
||||||
if (msg.kind === OVERLAY_MSG_WHO_AM_I) {
|
if (msg.kind === OVERLAY_MSG_WHO_AM_I) {
|
||||||
const tabId = sender.tab?.id;
|
|
||||||
const windowId = sender.tab?.windowId;
|
const windowId = sender.tab?.windowId;
|
||||||
const ctx = typeof windowId === "number" ? sessions.findByWindowId(windowId) : null;
|
const ctx = typeof windowId === "number" ? sessions.findByWindowId(windowId) : null;
|
||||||
const sessionId =
|
sendResponse({ sessionId: ctx?.sessionId ?? null });
|
||||||
ctx && typeof tabId === "number" && isAgentControlledTab(ctx, tabId) ? ctx.sessionId : null;
|
|
||||||
sendResponse({ sessionId });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,15 @@ describe("SessionManager", () => {
|
|||||||
reservation.commit({ tabId: 42, originalWindowId: 7, originalIndex: 3 });
|
reservation.commit({ tabId: 42, originalWindowId: 7, originalIndex: 3 });
|
||||||
expect(isAgentControlledTab(ctx, 42)).toBe(true);
|
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", () => {
|
describe("findBorrowingSession", () => {
|
||||||
|
|||||||
@@ -117,6 +117,13 @@ export class SessionManager {
|
|||||||
return Array.from(this.sessions.values());
|
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*
|
* Look up whether `tabId` is currently borrowed by some *other*
|
||||||
* session than the one calling. Used by M8 `tab_borrow` to refuse
|
* session than the one calling. Used by M8 `tab_borrow` to refuse
|
||||||
|
|||||||
@@ -458,6 +458,16 @@ describe("ToolDispatcher", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("invokes onBrowserControlResumed for browser-control tools but not passive reads", async () => {
|
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 { transport, sent, deliver } = fakeTransport();
|
||||||
const sessions = new SessionManager({
|
const sessions = new SessionManager({
|
||||||
agentWindow: {
|
agentWindow: {
|
||||||
@@ -467,7 +477,13 @@ describe("ToolDispatcher", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const onBrowserControlResumed = vi.fn();
|
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();
|
dispatcher.start();
|
||||||
|
|
||||||
deliver(makeRequest("tool.session_start", { session_id: "aa11" }));
|
deliver(makeRequest("tool.session_start", { session_id: "aa11" }));
|
||||||
@@ -484,6 +500,7 @@ describe("ToolDispatcher", () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
expect(onBrowserControlResumed).toHaveBeenCalledWith("aa11");
|
expect(onBrowserControlResumed).toHaveBeenCalledWith("aa11");
|
||||||
|
expect(onAgentTabClaimed).toHaveBeenCalledWith(7, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reasserts remembered hover before follow-up work and releases only after actions", async () => {
|
it("reasserts remembered hover before follow-up work and releases only after actions", async () => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
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 () => {
|
it("targets an explicit tab_id", async () => {
|
||||||
const sm = await makeManager();
|
const sm = await makeManager();
|
||||||
sm.get("aa11")?.agentCreatedTabs.add(9);
|
|
||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
const result = await handleEmulate(
|
const result = await handleEmulate(
|
||||||
sm,
|
sm,
|
||||||
@@ -277,7 +276,6 @@ describe("handleEmulate", () => {
|
|||||||
|
|
||||||
it("keeps emulation state per tab", async () => {
|
it("keeps emulation state per tab", async () => {
|
||||||
const sm = await makeManager();
|
const sm = await makeManager();
|
||||||
sm.get("aa11")?.agentCreatedTabs.add(9);
|
|
||||||
const deps = makeDeps();
|
const deps = makeDeps();
|
||||||
await handleEmulate(sm, { session_id: "aa11", overrides: fullOverrides }, deps);
|
await handleEmulate(sm, { session_id: "aa11", overrides: fullOverrides }, deps);
|
||||||
await handleEmulate(
|
await handleEmulate(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
remove: vi.fn(async () => {}),
|
||||||
ensureActiveTab: vi.fn(async () => 4),
|
ensureActiveTab: vi.fn(async () => 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,16 +52,10 @@ function installHelpLifecycleChrome() {
|
|||||||
|
|
||||||
function fakeManager(sessionId: string, agentWindowId: number, tabId: number) {
|
function fakeManager(sessionId: string, agentWindowId: number, tabId: number) {
|
||||||
const refStore = new RefStore();
|
const refStore = new RefStore();
|
||||||
const session = {
|
|
||||||
sessionId,
|
|
||||||
agentWindowId,
|
|
||||||
refStore,
|
|
||||||
borrowedTabs: new Map(),
|
|
||||||
agentCreatedTabs: new Set([tabId]),
|
|
||||||
};
|
|
||||||
const mgr = {
|
const mgr = {
|
||||||
get: (id: string) => (id === sessionId ? session : null),
|
get: (id: string) =>
|
||||||
findByWindowId: (wid: number) => (wid === agentWindowId ? session : null),
|
id === sessionId ? { sessionId, agentWindowId, refStore, borrowedTabs: new Map() } : null,
|
||||||
|
findByWindowId: (wid: number) => (wid === agentWindowId ? { sessionId } : null),
|
||||||
} as unknown as SessionManager;
|
} as unknown as SessionManager;
|
||||||
return mgr;
|
return mgr;
|
||||||
}
|
}
|
||||||
@@ -363,7 +357,6 @@ describe("handleRequestHelp", () => {
|
|||||||
agentWindowId: 99,
|
agentWindowId: 99,
|
||||||
refStore,
|
refStore,
|
||||||
borrowedTabs: new Map(),
|
borrowedTabs: new Map(),
|
||||||
agentCreatedTabs: new Set([5]),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
||||||
@@ -420,7 +413,6 @@ describe("handleRequestHelp", () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
borrowedTabs: new Map(),
|
borrowedTabs: new Map(),
|
||||||
agentCreatedTabs: new Set([5]),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
||||||
@@ -506,7 +498,6 @@ describe("handleRequestHelp", () => {
|
|||||||
agentWindowId: 99,
|
agentWindowId: 99,
|
||||||
refStore,
|
refStore,
|
||||||
borrowedTabs: new Map(),
|
borrowedTabs: new Map(),
|
||||||
agentCreatedTabs: new Set([5]),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
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 sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
||||||
ctx.agentCreatedTabs.add(5);
|
|
||||||
const fake = makeFakeCdp({});
|
const fake = makeFakeCdp({});
|
||||||
const res = await handleClick(
|
const res = await handleClick(
|
||||||
sm,
|
sm,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
remove: vi.fn(async () => {}),
|
||||||
ensureActiveTab: vi.fn(async () => 4),
|
ensureActiveTab: vi.fn(async () => 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
remove: vi.fn(async () => {}),
|
||||||
ensureActiveTab: vi.fn(async () => 4),
|
ensureActiveTab: vi.fn(async () => 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ function fakeManager() {
|
|||||||
agentWindowId: AGENT_WINDOW_ID,
|
agentWindowId: AGENT_WINDOW_ID,
|
||||||
refStore: { resolve: () => null, replace: () => {} },
|
refStore: { resolve: () => null, replace: () => {} },
|
||||||
borrowedTabs: new Map(),
|
borrowedTabs: new Map(),
|
||||||
agentCreatedTabs: new Set([TAB_ID]),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
findByWindowId: (windowId: number) =>
|
findByWindowId: (windowId: number) =>
|
||||||
|
|||||||
@@ -301,8 +301,6 @@ describe("handleSessionStop window release (issue #57)", () => {
|
|||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11);
|
ctx.agentCreatedTabs.add(11);
|
||||||
ctx.agentCreatedTabs.add(12);
|
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)
|
// user tab 99 (created via Chrome UI, NOT in agentCreatedTabs)
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([
|
tabs: new Map([
|
||||||
@@ -340,7 +338,6 @@ describe("handleSessionStop window release (issue #57)", () => {
|
|||||||
const sm = new SessionManager({ agentWindow: aw });
|
const sm = new SessionManager({ agentWindow: aw });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11);
|
ctx.agentCreatedTabs.add(11);
|
||||||
ctx.agentCreatedTabs.add(10); // home tab
|
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([
|
tabs: new Map([
|
||||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
[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 sm = new SessionManager({ agentWindow: aw });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11); // no longer in state → remove throws
|
ctx.agentCreatedTabs.add(11); // no longer in state → remove throws
|
||||||
ctx.agentCreatedTabs.add(10); // home tab
|
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab]]),
|
tabs: new Map([[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab]]),
|
||||||
windowsClosed: new Set(),
|
windowsClosed: new Set(),
|
||||||
@@ -399,7 +395,6 @@ describe("handleSessionStop window release (issue #57)", () => {
|
|||||||
const sm = new SessionManager({ agentWindow: aw });
|
const sm = new SessionManager({ agentWindow: aw });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11);
|
ctx.agentCreatedTabs.add(11);
|
||||||
ctx.agentCreatedTabs.add(10); // home tab
|
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([
|
tabs: new Map([
|
||||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
[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 sm = new SessionManager({ agentWindow: aw });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11);
|
ctx.agentCreatedTabs.add(11);
|
||||||
ctx.agentCreatedTabs.add(10); // home tab
|
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([[11, { id: 11, windowId: agentWindowId } as chrome.tabs.Tab]]),
|
tabs: new Map([[11, { id: 11, windowId: agentWindowId } as chrome.tabs.Tab]]),
|
||||||
windowsClosed: new Set(),
|
windowsClosed: new Set(),
|
||||||
@@ -473,7 +467,6 @@ describe("handleSessionStop window release (issue #57)", () => {
|
|||||||
const sm = new SessionManager({ agentWindow: aw });
|
const sm = new SessionManager({ agentWindow: aw });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(11); // Step 4 remove() will throw for this tab
|
ctx.agentCreatedTabs.add(11); // Step 4 remove() will throw for this tab
|
||||||
ctx.agentCreatedTabs.add(10); // home tab
|
|
||||||
const state: FakeState = {
|
const state: FakeState = {
|
||||||
tabs: new Map([
|
tabs: new Map([
|
||||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
||||||
|
|||||||
@@ -60,24 +60,19 @@ describe("handleTabList", () => {
|
|||||||
expect(res.tabs.map((t) => t.scope).sort()).toEqual(["agent", "user"]);
|
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]) });
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
await sm.start("aa11");
|
await sm.start("aa11");
|
||||||
const tabs = fakeChromeTabsApi([
|
const tabs = fakeChromeTabsApi([
|
||||||
{ windowId: 100, url: "chrome://newtab/", title: "Agent New Tab", active: true },
|
{ 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://example.com", title: "Example", active: true },
|
||||||
{ windowId: 200, url: "https://github.com", title: "GitHub", active: false },
|
{ windowId: 200, url: "https://github.com", title: "GitHub", active: false },
|
||||||
]);
|
]);
|
||||||
const res = await handleTabList(sm, { session_id: "aa11", scope: "user" }, tabs);
|
const res = await handleTabList(sm, { session_id: "aa11", scope: "user" }, tabs);
|
||||||
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
|
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.every((t) => t.scope === "user")).toBe(true);
|
||||||
expect(res.tabs.map((t) => t.url)).toEqual([
|
expect(res.tabs.map((t) => t.url)).toEqual(["https://example.com", "https://github.com"]);
|
||||||
"https://user.example/",
|
|
||||||
"https://example.com",
|
|
||||||
"https://github.com",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scope=agent returns only the requesting session's Agent Window tabs", async () => {
|
it("scope=agent returns only the requesting session's Agent Window tabs", async () => {
|
||||||
@@ -302,8 +297,7 @@ describe("handleTabCreate", () => {
|
|||||||
describe("handleTabClose", () => {
|
describe("handleTabClose", () => {
|
||||||
it("removes a tab inside the Agent Window", async () => {
|
it("removes a tab inside the Agent Window", async () => {
|
||||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
const ctx = await sm.start("aa11");
|
await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(5);
|
|
||||||
const state: FakeTabState = {
|
const state: FakeTabState = {
|
||||||
tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]),
|
tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]),
|
||||||
nextTabId: 50,
|
nextTabId: 50,
|
||||||
@@ -316,23 +310,6 @@ describe("handleTabClose", () => {
|
|||||||
expect(spies.remove).toHaveBeenCalledWith(5);
|
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 () => {
|
it("rejects closing a borrowed tab — caller must tab_return first", async () => {
|
||||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
const ctx = await sm.start("aa11");
|
const ctx = await sm.start("aa11");
|
||||||
@@ -383,8 +360,7 @@ describe("handleTabClose", () => {
|
|||||||
describe("handleTabSelect", () => {
|
describe("handleTabSelect", () => {
|
||||||
it("activates a tab inside the Agent Window", async () => {
|
it("activates a tab inside the Agent Window", async () => {
|
||||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
const ctx = await sm.start("aa11");
|
await sm.start("aa11");
|
||||||
ctx.agentCreatedTabs.add(3);
|
|
||||||
const state: FakeTabState = {
|
const state: FakeTabState = {
|
||||||
tabs: new Map([[3, { id: 3, windowId: 100 } as chrome.tabs.Tab]]),
|
tabs: new Map([[3, { id: 3, windowId: 100 } as chrome.tabs.Tab]]),
|
||||||
nextTabId: 50,
|
nextTabId: 50,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) {
|
|||||||
return id;
|
return id;
|
||||||
}),
|
}),
|
||||||
remove: vi.fn(async () => {}),
|
remove: vi.fn(async () => {}),
|
||||||
ensureActiveTab: vi.fn(async () => 4),
|
ensureActiveTab: vi.fn(async () => 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export interface DispatcherDeps {
|
|||||||
onSessionsChanged?: () => void;
|
onSessionsChanged?: () => void;
|
||||||
/** Invoked before a tool that dispatches page input or mutates browser state is forwarded. */
|
/** Invoked before a tool that dispatches page input or mutates browser state is forwarded. */
|
||||||
onBrowserControlResumed?: (sessionId: string) => void;
|
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). */
|
/** User approval for `tool.tab_borrow` (overlay in content script). */
|
||||||
approveBorrow?: BorrowConfirmationApprover;
|
approveBorrow?: BorrowConfirmationApprover;
|
||||||
/** i18n notification copy for `tool.request_help` (resolved per-call). */
|
/** i18n notification copy for `tool.request_help` (resolved per-call). */
|
||||||
@@ -144,6 +146,7 @@ export class ToolDispatcher {
|
|||||||
private readonly recording?: RecordRuntimeDeps;
|
private readonly recording?: RecordRuntimeDeps;
|
||||||
private readonly onSessionsChanged?: () => void;
|
private readonly onSessionsChanged?: () => void;
|
||||||
private readonly onBrowserControlResumed?: (sessionId: string) => void;
|
private readonly onBrowserControlResumed?: (sessionId: string) => void;
|
||||||
|
private readonly onAgentTabClaimed?: (tabId: number, windowId: number) => void;
|
||||||
private readonly approveBorrow?: BorrowConfirmationApprover;
|
private readonly approveBorrow?: BorrowConfirmationApprover;
|
||||||
private readonly helpNotificationCopy?: () => { title: string; body: string };
|
private readonly helpNotificationCopy?: () => { title: string; body: string };
|
||||||
private subscription: { dispose(): void } | null = null;
|
private subscription: { dispose(): void } | null = null;
|
||||||
@@ -164,6 +167,7 @@ export class ToolDispatcher {
|
|||||||
this.recording = deps.recording;
|
this.recording = deps.recording;
|
||||||
this.onSessionsChanged = deps.onSessionsChanged;
|
this.onSessionsChanged = deps.onSessionsChanged;
|
||||||
this.onBrowserControlResumed = deps.onBrowserControlResumed;
|
this.onBrowserControlResumed = deps.onBrowserControlResumed;
|
||||||
|
this.onAgentTabClaimed = deps.onAgentTabClaimed;
|
||||||
this.approveBorrow = deps.approveBorrow;
|
this.approveBorrow = deps.approveBorrow;
|
||||||
this.helpNotificationCopy = deps.helpNotificationCopy;
|
this.helpNotificationCopy = deps.helpNotificationCopy;
|
||||||
}
|
}
|
||||||
@@ -309,8 +313,15 @@ export class ToolDispatcher {
|
|||||||
}
|
}
|
||||||
case "tool.tab_list":
|
case "tool.tab_list":
|
||||||
return handleTabList(this.sessions, req.params as TabListParams, chromeTabsApi, signal);
|
return handleTabList(this.sessions, req.params as TabListParams, chromeTabsApi, signal);
|
||||||
case "tool.tab_create":
|
case "tool.tab_create": {
|
||||||
return handleTabCreate(this.sessions, req.params as TabCreateParams, { signal });
|
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":
|
case "tool.tab_close":
|
||||||
return this.withHoverReleaseForRequest(
|
return this.withHoverReleaseForRequest(
|
||||||
req.params as TabCloseParams,
|
req.params as TabCloseParams,
|
||||||
@@ -319,11 +330,16 @@ export class ToolDispatcher {
|
|||||||
);
|
);
|
||||||
case "tool.tab_select":
|
case "tool.tab_select":
|
||||||
return handleTabSelect(this.sessions, req.params as TabSelectParams, { signal });
|
return handleTabSelect(this.sessions, req.params as TabSelectParams, { signal });
|
||||||
case "tool.tab_borrow":
|
case "tool.tab_borrow": {
|
||||||
return handleTabBorrow(this.sessions, req.params as TabBorrowParams, {
|
const result = await handleTabBorrow(this.sessions, req.params as TabBorrowParams, {
|
||||||
signal,
|
signal,
|
||||||
approveBorrow: this.approveBorrow,
|
approveBorrow: this.approveBorrow,
|
||||||
});
|
});
|
||||||
|
if (!isRpcError(result)) {
|
||||||
|
this.onAgentTabClaimed?.(result.tab_id, result.agent_window_id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
case "tool.tab_return":
|
case "tool.tab_return":
|
||||||
return handleTabReturn(this.sessions, req.params as TabReturnParams, { signal });
|
return handleTabReturn(this.sessions, req.params as TabReturnParams, { signal });
|
||||||
case "tool.window_resize":
|
case "tool.window_resize":
|
||||||
|
|||||||
@@ -8,11 +8,7 @@
|
|||||||
|
|
||||||
import type { CdpDebuggee, DialogCursor } from "@/browser-driver/chromium-cdp";
|
import type { CdpDebuggee, DialogCursor } from "@/browser-driver/chromium-cdp";
|
||||||
import type { CdpFrameGraph, CdpTarget } from "@/browser-driver/frame-graph";
|
import type { CdpFrameGraph, CdpTarget } from "@/browser-driver/frame-graph";
|
||||||
import {
|
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
||||||
isAgentControlledTab,
|
|
||||||
type SessionContext,
|
|
||||||
type SessionManager,
|
|
||||||
} from "@/session-manager/manager";
|
|
||||||
import { normaliseRef } from "@/session-manager/ref-store";
|
import { normaliseRef } from "@/session-manager/ref-store";
|
||||||
import type { ConsoleResult, JavaScriptDialogInfo, RpcError } from "@/transport/types";
|
import type { ConsoleResult, JavaScriptDialogInfo, RpcError } from "@/transport/types";
|
||||||
import { rpcError } from "./errors";
|
import { rpcError } from "./errors";
|
||||||
@@ -327,22 +323,23 @@ export async function resolveCdpAccessibleTargetTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sandbox guard: M7 write tools (click / fill / press / navigate*) must
|
* Sandbox guard: M7 write tools (click / fill / press / navigate*)
|
||||||
* refuse tabs the session has not explicitly claimed. A controlled tab must
|
* MUST refuse to touch a tab outside the session's Agent Window
|
||||||
* also remain inside 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(
|
export function enforceAgentWindow(
|
||||||
ctx: SessionContext,
|
ctx: SessionContext,
|
||||||
target: { tabId: number; windowId: number },
|
target: { tabId: number; windowId: number },
|
||||||
toolName: string,
|
toolName: string,
|
||||||
): RpcError | null {
|
): RpcError | null {
|
||||||
if (target.windowId !== ctx.agentWindowId || !isAgentControlledTab(ctx, target.tabId)) {
|
if (target.windowId !== ctx.agentWindowId) {
|
||||||
return rpcError(
|
return rpcError(
|
||||||
"permission_denied",
|
"permission_denied",
|
||||||
"agent_window_scope",
|
"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;
|
return null;
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import {
|
|||||||
OVERLAY_AGENT_OVERLAY_RESET,
|
OVERLAY_AGENT_OVERLAY_RESET,
|
||||||
type OverlayAgentOverlayResetMessage,
|
type OverlayAgentOverlayResetMessage,
|
||||||
} from "@/lib/overlay-bridge";
|
} from "@/lib/overlay-bridge";
|
||||||
import {
|
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
||||||
isAgentControlledTab,
|
|
||||||
type SessionContext,
|
|
||||||
type SessionManager,
|
|
||||||
} from "@/session-manager/manager";
|
|
||||||
import type { RpcError } from "@/transport/types";
|
import type { RpcError } from "@/transport/types";
|
||||||
import { rpcError } from "./errors";
|
import { rpcError } from "./errors";
|
||||||
import { isRpcError, lookupSession } from "./shared";
|
import { isRpcError, lookupSession } from "./shared";
|
||||||
@@ -29,9 +25,11 @@ export interface TabInfo {
|
|||||||
window_id?: number;
|
window_id?: number;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
/**
|
/**
|
||||||
* Ownership relative to the requesting session. Explicitly created or
|
* Where the tab sits relative to the requesting session: tabs in any
|
||||||
* borrowed tabs in this session's Agent Window are `agent`; every other
|
* window other than an Agent Window are `user`; tabs in this
|
||||||
* visible tab is `user`. Other sessions' Agent Windows are filtered out.
|
* 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";
|
scope?: "user" | "agent";
|
||||||
}
|
}
|
||||||
@@ -261,8 +259,7 @@ export async function handleTabList(
|
|||||||
if (typeof t.id !== "number") continue;
|
if (typeof t.id !== "number") continue;
|
||||||
const winId = typeof t.windowId === "number" ? t.windowId : -1;
|
const winId = typeof t.windowId === "number" ? t.windowId : -1;
|
||||||
if (otherAgentWindowIds.has(winId)) continue;
|
if (otherAgentWindowIds.has(winId)) continue;
|
||||||
const tabScope: "user" | "agent" =
|
const tabScope: "user" | "agent" = winId === myAgentWindowId ? "agent" : "user";
|
||||||
winId === myAgentWindowId && isAgentControlledTab(ctx, t.id) ? "agent" : "user";
|
|
||||||
if (scope === "user" && tabScope !== "user") continue;
|
if (scope === "user" && tabScope !== "user") continue;
|
||||||
if (scope === "agent" && tabScope !== "agent") continue;
|
if (scope === "agent" && tabScope !== "agent") continue;
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -484,13 +481,13 @@ async function authoriseAgentTab(
|
|||||||
`${toolName}: tab ${tabId} is borrowed by session ${otherBorrower}`,
|
`${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 tab;
|
||||||
}
|
}
|
||||||
return rpcError(
|
return rpcError(
|
||||||
"permission_denied",
|
"permission_denied",
|
||||||
"agent_window_scope",
|
"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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user