fix(cli): validate agentId in fn_task_create/update and unblock bundle tests

fn_task_create and fn_task_update accepted any string as `agentId` and
wrote it verbatim onto `task.assignedAgentId`, letting hallucinated IDs
(e.g. `agent-executor-001`) appear as agent badges in the dashboard.
Mirror the validation already used by fn_delegate: look the agent up via
AgentStore and reject unknown or ephemeral/runtime-managed agents. Null
still clears the field on update.

Also clean up two stale failures in bundle-output.test that predated this
change:
- pi-claude-cli no longer imports cross-spawn, so drop the dependency and
  its orphan type-decl file.
- Loosen the spawn-import regex to match `spawn` anywhere in the
  destructured import (the source has additional named imports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 21:34:34 -07:00
parent afef995eb7
commit c14f2a3991
6 changed files with 118 additions and 22 deletions

View File

@@ -119,7 +119,7 @@ describe("CLI bundle output", () => {
it("pi-claude-cli source imports spawn from node:child_process", () => {
const processManagerSource = readFileSync(join(cliRoot, "dist", "pi-claude-cli", "src", "process-manager.ts"), "utf-8");
expect(processManagerSource).toMatch(/import\s+\{\s*spawn[\s\S]*\}\s+from\s*["']node:child_process["']/);
expect(processManagerSource).toMatch(/import\s+\{[^}]*\bspawn\b[^}]*\}\s+from\s*["']node:child_process["']/);
});
it("pi-claude-cli package.json does not require cross-spawn dependency", () => {

View File

@@ -27,7 +27,7 @@ vi.mock("../commands/task.js", () => ({
}));
import kbExtension from "../extension.js";
import { TaskStore } from "@fusion/core";
import { TaskStore, AgentStore } from "@fusion/core";
import { isGhAvailable, isGhAuthenticated, runGhJsonAsync } from "@fusion/core/gh-cli";
import { runTaskPlan } from "../commands/task.js";
@@ -80,6 +80,20 @@ function makeCtx(cwd: string) {
return { cwd } as any;
}
async function seedAgent(
cwd: string,
overrides: { ephemeral?: boolean; name?: string } = {},
): Promise<string> {
const agentStore = new AgentStore({ rootDir: join(cwd, ".fusion") });
await agentStore.init();
const agent = await agentStore.createAgent({
name: overrides.name ?? "test-agent",
role: "executor",
metadata: overrides.ephemeral ? { agentKind: "task-worker" } : {},
});
return agent.id;
}
async function removeDirWithRetries(path: string) {
const maxAttempts = 4;
@@ -250,23 +264,53 @@ describe.skip("fn pi extension", () => {
});
it("creates a task with assigned agent ID", async () => {
const agentId = await seedAgent(tmpDir);
const tool = api.tools.get("fn_task_create")!;
const result = await tool.execute(
"call-1",
{ description: "Task with assignee", agentId: "agent-abc123" },
{ description: "Task with assignee", agentId },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.details.taskId).toBe("FN-001");
expect(result.details.assignedAgentId).toBe("agent-abc123");
expect(result.content[0].text).toContain("Assigned to: agent-abc123");
expect(result.details.assignedAgentId).toBe(agentId);
expect(result.content[0].text).toContain(`Assigned to: ${agentId}`);
// Verify persistence via show
const showTool = api.tools.get("fn_task_show")!;
const show = await showTool.execute("s1", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
expect(show.details.task.assignedAgentId).toBe("agent-abc123");
expect(show.details.task.assignedAgentId).toBe(agentId);
});
it("rejects unknown agent IDs", async () => {
const tool = api.tools.get("fn_task_create")!;
const result = await tool.execute(
"call-1",
{ description: "Task with bogus assignee", agentId: "agent-does-not-exist" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Agent agent-does-not-exist not found");
});
it("rejects ephemeral/runtime-managed agents", async () => {
const ephemeralId = await seedAgent(tmpDir, { ephemeral: true, name: "task-worker" });
const tool = api.tools.get("fn_task_create")!;
const result = await tool.execute(
"call-1",
{ description: "Task with worker assignee", agentId: ephemeralId },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("ephemeral/runtime agent");
});
it("creates a task without assigned agent ID by default", async () => {
@@ -364,10 +408,11 @@ describe.skip("fn pi extension", () => {
const createTool = api.tools.get("fn_task_create")!;
await createTool.execute("c1", { description: "Original" }, undefined, undefined, makeCtx(tmpDir));
const agentId = await seedAgent(tmpDir);
const updateTool = api.tools.get("fn_task_update")!;
const result = await updateTool.execute(
"u1",
{ id: "FN-001", agentId: "agent-abc123" },
{ id: "FN-001", agentId },
undefined,
undefined,
makeCtx(tmpDir),
@@ -379,14 +424,32 @@ describe.skip("fn pi extension", () => {
const showTool = api.tools.get("fn_task_show")!;
const show = await showTool.execute("s1", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
expect(show.details.task.assignedAgentId).toBe("agent-abc123");
expect(show.details.task.assignedAgentId).toBe(agentId);
});
it("rejects unknown agent IDs on update", async () => {
const createTool = api.tools.get("fn_task_create")!;
await createTool.execute("c1", { description: "Original" }, undefined, undefined, makeCtx(tmpDir));
const updateTool = api.tools.get("fn_task_update")!;
const result = await updateTool.execute(
"u1",
{ id: "FN-001", agentId: "agent-does-not-exist" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Agent agent-does-not-exist not found");
});
it("clears task assigned agent ID with null", async () => {
const agentId = await seedAgent(tmpDir);
const createTool = api.tools.get("fn_task_create")!;
await createTool.execute(
"c1",
{ description: "Original", agentId: "agent-abc123" },
{ description: "Original", agentId },
undefined,
undefined,
makeCtx(tmpDir),