mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(dsh-plugin-browserskill): strict ownership boundary — foreign sessions are invisible
Tighten the shared-daemon boundary from 'cleanup only touches owned sessions' to 'the plugin only ever sees and operates on sessions it created': - browser_session_list no longer queries the daemon at all; it returns the plugin's own session table (with the current marker), so foreign sessions cannot even be enumerated through the plugin. - Every tool's optional session argument must name a plugin-created session; foreign/unknown ids are rejected with a clear error before any command reaches the daemon (the reference-adoption channel is gone). - Unchanged: dispose stops exactly the owned set, stop requires ownership, the start reservation protocol, and handle-precise child kills. Tests updated (59): explicit-foreign rejection without daemon contact, registry-only listing, reservation race, stale-handle-tolerant dispose. e2e: a manually started foreign session was rejected on reference, absent from session_list, refused on stop, and survived dispose while both plugin sessions were cleaned up.
This commit is contained in:
@@ -75,8 +75,10 @@ describe("dispose cleanup ownership", () => {
|
||||
if (start === undefined || snapshot === undefined) throw new Error("tools not registered");
|
||||
await start.execute({}, makeExec());
|
||||
await start.execute({}, makeExec());
|
||||
// A foreign session gets referenced (current-pointer bookkeeping) but never owned.
|
||||
await snapshot.execute({ session: "ext9" }, makeExec());
|
||||
// A foreign session cannot even be referenced any more.
|
||||
await expect(snapshot.execute({ session: "ext9" }, makeExec())).rejects.toThrow(
|
||||
/does not belong to this plugin/,
|
||||
);
|
||||
|
||||
await Promise.all(disposers.map((dispose) => dispose()));
|
||||
|
||||
|
||||
@@ -45,13 +45,22 @@ describe("SessionRegistry", () => {
|
||||
expect(registry.ownedIds()).toEqual(["s1"]);
|
||||
});
|
||||
|
||||
it("resolve prefers the explicit session and adopts it as a reference", () => {
|
||||
it("resolve accepts an explicit owned session and makes it current", () => {
|
||||
const registry = new SessionRegistry(5);
|
||||
start(registry, "s1");
|
||||
expect(registry.resolve("foreign", "tool")).toBe("foreign");
|
||||
expect(registry.current()).toBe("foreign");
|
||||
start(registry, "s2");
|
||||
expect(registry.resolve("s1", "tool")).toBe("s1");
|
||||
expect(registry.current()).toBe("s1");
|
||||
});
|
||||
|
||||
it("resolve rejects foreign ids and never adopts them", () => {
|
||||
const registry = new SessionRegistry(5);
|
||||
start(registry, "s1");
|
||||
expect(() => registry.resolve("foreign", "tool")).toThrow(/does not belong to this plugin/);
|
||||
expect(registry.isOwned("foreign")).toBe(false);
|
||||
expect(registry.ownedIds()).toEqual(["s1"]);
|
||||
// A rejected resolve must not move the current pointer.
|
||||
expect(registry.current()).toBe("s1");
|
||||
});
|
||||
|
||||
it("resolve falls back to the current session", () => {
|
||||
@@ -65,11 +74,11 @@ describe("SessionRegistry", () => {
|
||||
expect(() => registry.resolve(undefined, "browser_click")).toThrow(/browser_session_start/);
|
||||
});
|
||||
|
||||
it("touch refreshes recency so fallback picks the most recent survivor", () => {
|
||||
it("recency order survives remove of the current session", () => {
|
||||
const registry = new SessionRegistry(5);
|
||||
start(registry, "s1");
|
||||
start(registry, "s2");
|
||||
registry.touch("s1");
|
||||
registry.resolve("s1", "tool");
|
||||
registry.remove("s1");
|
||||
expect(registry.current()).toBe("s2");
|
||||
});
|
||||
@@ -79,9 +88,8 @@ describe("SessionRegistry", () => {
|
||||
start(registry, "s1");
|
||||
expect(registry.resolveForStop(undefined)).toBe("s1");
|
||||
expect(registry.resolveForStop("s1")).toBe("s1");
|
||||
registry.resolve("foreign", "browser_snapshot");
|
||||
expect(() => registry.resolveForStop("foreign")).toThrow(/not created by this plugin/);
|
||||
expect(() => registry.resolveForStop("never-seen")).toThrow(/not created by this plugin/);
|
||||
expect(() => registry.resolveForStop("foreign")).toThrow(/does not belong to this plugin/);
|
||||
expect(() => registry.resolveForStop("never-seen")).toThrow(/does not belong to this plugin/);
|
||||
});
|
||||
|
||||
it("a refused stop does not move the current pointer", () => {
|
||||
@@ -91,12 +99,10 @@ describe("SessionRegistry", () => {
|
||||
expect(registry.current()).toBe("s1");
|
||||
});
|
||||
|
||||
it("ownedIds excludes referenced sessions even after heavy interleaving", () => {
|
||||
it("ownedIds always equals every registered session (foreign ids never enter)", () => {
|
||||
const registry = new SessionRegistry(5);
|
||||
start(registry, "own1");
|
||||
registry.resolve("ext1", "tool");
|
||||
start(registry, "own2");
|
||||
registry.resolve("ext2", "tool");
|
||||
expect(registry.ownedIds().sort()).toEqual(["own1", "own2"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,16 +208,31 @@ describe("multi-session behavior", () => {
|
||||
expect(calls[1].args).toEqual(["snapshot", "--session", "s1"]);
|
||||
});
|
||||
|
||||
it("an explicit session wins and becomes the new current session", async () => {
|
||||
const { tools, calls, registry } = setup(responses);
|
||||
await startSession(tools);
|
||||
it("an explicit owned session wins and becomes the new current session", async () => {
|
||||
const { tools, calls, registry } = setup({
|
||||
"session start": seq([START_REPLY("s1"), START_REPLY("s2")]),
|
||||
snapshot: SNAPSHOT_REPLY,
|
||||
});
|
||||
await startSession(tools, "s1");
|
||||
await startSession(tools, "s2");
|
||||
const snapshot = tools.get("browser_snapshot");
|
||||
const value = (await snapshot?.execute({ session: "other" }, makeExec())) as {
|
||||
const value = (await snapshot?.execute({ session: "s1" }, makeExec())) as {
|
||||
session: string;
|
||||
};
|
||||
expect(value.session).toBe("other");
|
||||
expect(calls[1].args).toEqual(["snapshot", "--session", "other"]);
|
||||
expect(registry.current()).toBe("other");
|
||||
expect(value.session).toBe("s1");
|
||||
expect(calls[2].args).toEqual(["snapshot", "--session", "s1"]);
|
||||
expect(registry.current()).toBe("s1");
|
||||
});
|
||||
|
||||
it("rejects an explicit foreign session without touching the daemon", async () => {
|
||||
const { tools, calls } = setup(responses);
|
||||
await startSession(tools);
|
||||
const callsBefore = calls.length;
|
||||
const snapshot = tools.get("browser_snapshot");
|
||||
await expect(snapshot?.execute({ session: "other" }, makeExec())).rejects.toThrow(
|
||||
/does not belong to this plugin/,
|
||||
);
|
||||
expect(calls.length).toBe(callsBefore);
|
||||
});
|
||||
|
||||
it("fails with guidance when no session exists", async () => {
|
||||
@@ -263,12 +278,9 @@ describe("multi-session behavior", () => {
|
||||
it("refuses to stop a session the plugin did not create", async () => {
|
||||
const { tools, calls } = setup(responses);
|
||||
await startSession(tools);
|
||||
// Reference a foreign session (adopted for the current pointer, never owned).
|
||||
const snapshot = tools.get("browser_snapshot");
|
||||
await snapshot?.execute({ session: "ext7" }, makeExec());
|
||||
const stop = tools.get("browser_session_stop");
|
||||
await expect(stop?.execute({ session: "ext7" }, makeExec())).rejects.toThrow(
|
||||
/not created by this plugin/,
|
||||
/does not belong to this plugin/,
|
||||
);
|
||||
// No stop command may reach the daemon for a foreign session.
|
||||
expect(calls.some((c) => c.args.join(" ").startsWith("session stop"))).toBe(false);
|
||||
@@ -289,23 +301,22 @@ describe("browser_session_stop / list", () => {
|
||||
expect(registry.current()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lists daemon sessions with the current marker", async () => {
|
||||
const { tools } = setup({
|
||||
"session start": START_REPLY("s1"),
|
||||
"session list": [
|
||||
{ session_id: "s1", browser_instance_id: "chrome-1" },
|
||||
{ session_id: "s2", browser_instance_id: "chrome-1" },
|
||||
],
|
||||
it("lists only plugin-owned sessions with the current marker (no daemon view)", async () => {
|
||||
const { tools, calls } = setup({
|
||||
"session start": seq([START_REPLY("s1"), START_REPLY("s2")]),
|
||||
});
|
||||
await startSession(tools);
|
||||
await startSession(tools);
|
||||
const list = tools.get("browser_session_list");
|
||||
const value = (await list?.execute({}, makeExec())) as {
|
||||
sessions: { sessionId: string; current: boolean }[];
|
||||
};
|
||||
expect(value.sessions).toEqual([
|
||||
{ sessionId: "s1", browserInstanceId: "chrome-1", current: true },
|
||||
{ sessionId: "s2", browserInstanceId: "chrome-1", current: false },
|
||||
{ sessionId: "s1", browserInstanceId: "chrome-1", current: false },
|
||||
{ sessionId: "s2", browserInstanceId: "chrome-1", current: true },
|
||||
]);
|
||||
// Registry-only: listing must not call the daemon at all.
|
||||
expect(calls.some((c) => c.args.join(" ").startsWith("session list"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -533,8 +544,8 @@ describe("error and cancellation semantics", () => {
|
||||
killAll() {},
|
||||
};
|
||||
registerTools({ ctx: ctx as never, runner, registry, config: CONFIG });
|
||||
const list = tools.get("browser_session_list");
|
||||
await expect(list?.execute({}, makeExec())).rejects.toThrow(/BrowserSkill must be installed/);
|
||||
const start = tools.get("browser_session_start");
|
||||
await expect(start?.execute({}, makeExec())).rejects.toThrow(/BrowserSkill must be installed/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user