diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 15:54:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 15:54:39 +0900 |
| commit | 3ff2db698c2633023934d8477a9e995f78fa011e (patch) | |
| tree | 1797ca96cb8fbc8df23a2667037e3a6ddeec2f35 /packages/api/tests | |
| parent | c0c08720cceb75b5e635e71190ae1f956f535133 (diff) | |
| download | dispatch-3ff2db698c2633023934d8477a9e995f78fa011e.tar.gz dispatch-3ff2db698c2633023934d8477a9e995f78fa011e.zip | |
fix(perm): decouple perm_user_agent from perm_summon for spawning user agents
Granting only the user-agent (top-level) permission without the
subagent-summon permission left the agent unable to summon user agents:
the whole summon tool was gated behind perm_summon, so perm_user_agent
alone produced no summon tool.
Register summon when EITHER perm_summon OR perm_user_agent is granted.
createSummonTool now takes an independent subagentEnabled flag (mirrors
perm_summon) alongside userAgentEnabled (mirrors perm_user_agent):
- subagent-only -> ordinary subagents, no top_level
- user-agent-only -> spawns ONLY top-level user agents (top_level
forced, background/top_level params dropped, user-agent catalog only)
- both -> unchanged full behavior
retrieve stays bundled with perm_summon (user agents are fire-and-forget).
Adds core summon tests (user-agent-only mode + legacy-default regression)
and an agent-manager summon/user_agent permission-split suite.
Diffstat (limited to 'packages/api/tests')
| -rw-r--r-- | packages/api/tests/agent-manager.test.ts | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts index 014022a..f3ea207 100644 --- a/packages/api/tests/agent-manager.test.ts +++ b/packages/api/tests/agent-manager.test.ts @@ -319,6 +319,22 @@ vi.mock("@dispatch/core", () => ({ execute: async () => "mock", }; }, + // Summon parent-path dependencies. The real implementations load agent + // definitions from disk; tests only need the summon/retrieve tool entries + // to appear, so these return empty projections. + loadAgents() { + return []; + }, + toAvailableSubagents() { + return []; + }, + toAvailableUserAgents() { + return []; + }, + getAgentDirPaths() { + return []; + }, + GLOBAL_AGENTS_DIR: "/tmp/global-agents", createTab() {}, getTab(id: string) { return fakeTabs.get(id) ?? null; @@ -1441,6 +1457,51 @@ describe("AgentManager", () => { }); }); + describe("summon / user_agent permission split", () => { + // Drives the real parent-path tool construction in + // getOrCreateAgentForTab by toggling perm_summon and perm_user_agent + // independently, then inspecting which tools the constructed Agent + // received. The summon tool must be registered when EITHER permission + // is granted; `retrieve` rides with the subagent permission only + // (user agents are fire-and-forget). + async function toolsForPerms(tabId: string, perms: Record<string, string>): Promise<string[]> { + for (const [k, v] of Object.entries(perms)) setFakeSetting(k, v); + const manager = new AgentManager(); + await manager.processMessage(tabId, "go"); + return constructedAgents.at(-1)?.toolNames ?? []; + } + + it("grants summon + retrieve when only perm_summon is allowed", async () => { + const tools = await toolsForPerms("tab-summon-only", { perm_summon: "allow" }); + expect(tools).toContain("summon"); + expect(tools).toContain("retrieve"); + }); + + it("grants summon WITHOUT retrieve when only perm_user_agent is allowed", async () => { + // Regression: granting only the user-agent permission used to leave + // the agent unable to summon user agents because the whole summon + // tool was gated behind perm_summon. + const tools = await toolsForPerms("tab-user-agent-only", { perm_user_agent: "allow" }); + expect(tools).toContain("summon"); + expect(tools).not.toContain("retrieve"); + }); + + it("grants summon + retrieve when both permissions are allowed", async () => { + const tools = await toolsForPerms("tab-summon-both", { + perm_summon: "allow", + perm_user_agent: "allow", + }); + expect(tools).toContain("summon"); + expect(tools).toContain("retrieve"); + }); + + it("grants neither summon nor retrieve when both permissions are off", async () => { + const tools = await toolsForPerms("tab-summon-neither", {}); + expect(tools).not.toContain("summon"); + expect(tools).not.toContain("retrieve"); + }); + }); + // ─── Usage side-channel persistence ────────────────────────────── // // `usage` AgentEvents (one per LLM round-trip) are persisted as invisible |
