summaryrefslogtreecommitdiffhomepage
path: root/packages/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api')
-rw-r--r--packages/api/src/agent-manager.ts36
-rw-r--r--packages/api/tests/agent-manager.test.ts61
2 files changed, 85 insertions, 12 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 85dd160..9499ce5 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -575,7 +575,13 @@ export class AgentManager {
});
}
toolEntries.push({ name: "todo", tool: createTaskListTool(tabAgent.taskList) });
- if (permSummon) {
+ // The `summon` tool is registered when EITHER the subagent
+ // permission (`perm_summon`) OR the user-agent permission
+ // (`perm_user_agent`) is granted — the two are independent.
+ // `perm_summon` enables ordinary subagent spawning; granting
+ // only `perm_user_agent` exposes summon in user-agent-only mode
+ // (spawns top-level user agents exclusively).
+ if (permSummon || permUserAgent) {
// Capture parent's allowed tool names for child permission enforcement
const parentAllowedTools = new Set(toolEntries.map((e) => e.name));
const allAgentDefs = loadAgents(workingDirectory);
@@ -609,19 +615,25 @@ export class AgentManager {
availableUserAgents,
agentDirPaths,
permUserAgent,
+ permSummon,
),
});
- toolEntries.push({
- name: "retrieve",
- tool: createRetrieveTool({
- getResult: (id) =>
- tabAgent.shellStore.has(id)
- ? tabAgent.shellStore.getResult(id)
- : tabAgent.transcriptStore.has(id)
- ? tabAgent.transcriptStore.getResult(id)
- : this.getChildResult(id),
- }),
- });
+ // `retrieve` collects subagent results. User agents are
+ // fire-and-forget, so it is bundled with the subagent
+ // permission only — a user-agent-only grant doesn't get it.
+ if (permSummon) {
+ toolEntries.push({
+ name: "retrieve",
+ tool: createRetrieveTool({
+ getResult: (id) =>
+ tabAgent.shellStore.has(id)
+ ? tabAgent.shellStore.getResult(id)
+ : tabAgent.transcriptStore.has(id)
+ ? tabAgent.transcriptStore.getResult(id)
+ : this.getChildResult(id),
+ }),
+ });
+ }
}
if (permSendToTab || permReadTab) {
const tabCommAllowed = new Set<string>();
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