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 765e41838c
commit 901d61fa85
6 changed files with 118 additions and 22 deletions

View File

@@ -70,6 +70,30 @@ function getFusionDir(cwd: string): string {
return join(resolveProjectRoot(cwd), ".fusion");
}
/**
* Validate an agent id supplied to task create/update tools.
* Returns null on success, or an error message describing why the id was rejected.
*
* Rejects unknown agents and ephemeral/runtime-managed agents — mirrors fn_delegate
* so callers can't park hallucinated or task-worker IDs in `task.assignedAgentId`.
*/
async function validateAssignableAgentId(
cwd: string,
agentId: string,
): Promise<string | null> {
const { AgentStore, isEphemeralAgent } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: getFusionDir(cwd) });
await agentStore.init();
const agent = await agentStore.getAgent(agentId);
if (!agent) {
return `Agent ${agentId} not found`;
}
if (isEphemeralAgent(agent)) {
return `Cannot assign task to ephemeral/runtime agent ${agentId}`;
}
return null;
}
function formatTaskLine(t: Task): string {
const label =
t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "");
@@ -167,6 +191,18 @@ export default function kbExtension(pi: ExtensionAPI) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const store = await getStore(ctx.cwd);
if (params.agentId !== undefined) {
const error = await validateAssignableAgentId(ctx.cwd, params.agentId);
if (error) {
return {
content: [{ type: "text", text: error }],
isError: true,
details: { error },
};
}
}
const task = await store.createTask({
description: params.description.trim(),
dependencies: params.depends,
@@ -273,6 +309,16 @@ export default function kbExtension(pi: ExtensionAPI) {
updatedFields.push("dependencies");
}
if (params.agentId !== undefined) {
if (params.agentId !== null) {
const error = await validateAssignableAgentId(ctx.cwd, params.agentId);
if (error) {
return {
content: [{ type: "text", text: error }],
isError: true,
details: { error },
};
}
}
updates.assignedAgentId = params.agentId;
updatedFields.push("agentId");
}