diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.test.ts index 7248a7a2f..f6d0f8e75 100644 --- a/src/agents/tools/browser-tool.test.ts +++ b/src/agents/tools/browser-tool.test.ts @@ -289,3 +289,35 @@ describe("browser tool snapshot labels", () => { expect(result?.content?.[1]).toMatchObject({ type: "image" }); }); }); + +describe("browser tool tab open and close behaviors", () => { + afterEach(() => { + vi.clearAllMocks(); + configMocks.loadConfig.mockReturnValue({ browser: {} }); + }); + + it("close a tab by targetId should be allowed in profile = 'clawd'", async () => { + const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); + await tool.execute?.("", { action: "close", targetId: "a real target id", profile: "clawd" }); + expect(browserClientMocks.browserCloseTab).toHaveBeenCalledWith( + "http://127.0.0.1:9999", + "a real target id", + { profile: "clawd" }, + ); + }); + + it("close a tab by targetId should be allowed in profile = 'chrome'", async () => { + const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); + await tool.execute?.("", { action: "close", targetId: "a real target id", profile: "clawd" }); + expect(browserClientMocks.browserCloseTab).toHaveBeenCalledWith( + "http://127.0.0.1:9999", + "a real target id", + { profile: "clawd" }, + ); + }); + + it("close a tab without targetId should throw error", async () => { + const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); + await expect(tool.execute?.("", { action: "close" })).rejects.toThrow(); + }); +}); diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index ccdfd7c0f..8aed5d918 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -407,7 +407,13 @@ export function createBrowserTool(opts?: { return jsonResult(result); } if (targetId) await browserCloseTab(baseUrl, targetId, { profile }); - else await browserAct(baseUrl, { kind: "close" }, { profile }); + else { + // Known issue: The 'close' action does not verify the ownership of the current tab before atempting to close it, leading to potential action conflicts in concurrent scenarios. For now, enforce a strict close action with targetId for proxyRequest: path = "/act". The following line is commented out to prevent accidental usage. Instead, we throw an error to inform the user. + // await browserAct(baseUrl, { kind: "close" }, { profile }); + throw new Error( + `Closing a tab without providing a targetId is not allowed. (But it was somehow allowed to do so in a previous version!). To avoid closing a tab not belonging to you, ensure you maintain a set of tabs by their targetId and provide a targetId when closing.`, + ); + } return jsonResult({ ok: true }); } case "snapshot": {