feat(FN-3140): add readonly custom tools and plugin SDK
This merge adds readonly custom tool preservation (FN-3140) with new plugin SDK types and documentation, fixes PluginManager responsive overflow (FN-3093), and integrates the fn-3065 branch with enhanced plugin authoring capabilities. The core plugin-types module was significantly expanded with 230+ Fusion-Task-Id: FN-3140
This commit is contained in:
5
.changeset/fn-3140-fix-readonly-customtools.md
Normal file
5
.changeset/fn-3140-fix-readonly-customtools.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix readonly `createFnAgent` sessions to preserve caller-supplied engine custom tools while still excluding host extensions. This restores delegation and memory tools for no-task heartbeat/reviewer readonly sessions without reopening host extension tool injection in summarizer flows.
|
||||
@@ -594,6 +594,50 @@ describe("createFnAgent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps caller customTools in readonly sessions", async () => {
|
||||
createReadOnlyToolsMock.mockReturnValueOnce([{ name: "read" }] as any);
|
||||
const delegationTool = {
|
||||
name: "fn_list_agents",
|
||||
label: "List Agents",
|
||||
description: "List available agents",
|
||||
parameters: {},
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
const { createFnAgent } = await import("../pi.js");
|
||||
await createFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "readonly",
|
||||
customTools: [delegationTool as any],
|
||||
});
|
||||
|
||||
const createSessionArgs = createAgentSessionMock.mock.calls[0]?.[0] as { customTools: Array<{ name: string }> };
|
||||
expect(createSessionArgs.customTools.map((tool) => tool.name)).toContain("fn_list_agents");
|
||||
});
|
||||
|
||||
it("keeps caller customTools in coding sessions", async () => {
|
||||
createCodingToolsMock.mockReturnValueOnce([{ name: "read" }, { name: "write" }] as any);
|
||||
const customTool = {
|
||||
name: "fn_heartbeat_done",
|
||||
label: "Heartbeat Done",
|
||||
description: "Complete heartbeat",
|
||||
parameters: {},
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
const { createFnAgent } = await import("../pi.js");
|
||||
await createFnAgent({
|
||||
cwd: "/tmp",
|
||||
systemPrompt: "test",
|
||||
tools: "coding",
|
||||
customTools: [customTool as any],
|
||||
});
|
||||
|
||||
const createSessionArgs = createAgentSessionMock.mock.calls[0]?.[0] as { customTools: Array<{ name: string }> };
|
||||
expect(createSessionArgs.customTools.map((tool) => tool.name)).toContain("fn_heartbeat_done");
|
||||
});
|
||||
|
||||
it("logs createFnAgent startup diagnostics without leaking cwd", async () => {
|
||||
const { piLog } = await import("../logger.js");
|
||||
const logSpy = vi.spyOn(piLog, "log").mockImplementation(() => {});
|
||||
|
||||
@@ -1146,12 +1146,13 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
});
|
||||
}
|
||||
|
||||
// `tools: "readonly"` MUST mean a hermetically sealed read-only session — no
|
||||
// way for the model to mutate state. Host extensions (`@runfusion/fusion`)
|
||||
// register write tools like `fn_task_create`, so they are deliberately
|
||||
// EXCLUDED in readonly mode. Caller-supplied `customTools` are also dropped
|
||||
// for the same reason. Without this, summarizer/compaction sessions could
|
||||
// call write tools and mutate the task board (see FN-3057/FN-3058 incident).
|
||||
// `tools: "readonly"` MUST mean a hermetically sealed read-only session with
|
||||
// respect to host extension injection. Host extensions (`@runfusion/fusion`)
|
||||
// can register write tools like `fn_task_create`, so they are deliberately
|
||||
// EXCLUDED in readonly mode. Caller-supplied `customTools` are preserved,
|
||||
// since heartbeat/reviewer flows explicitly provide engine-owned tools.
|
||||
// This keeps summarizer/compaction sessions safe while retaining intended
|
||||
// delegation/memory tools for readonly engine sessions.
|
||||
const isReadonly = options.tools === "readonly";
|
||||
const effectiveExtensionPaths = isReadonly ? [] : hostExtensionPaths;
|
||||
if (isReadonly && hostExtensionPaths.length > 0) {
|
||||
@@ -1178,15 +1179,10 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
// suppress the defaults with `noTools: "builtin"` and register our wrapped
|
||||
// tools through `customTools` instead. The wrapped tools preserve the same
|
||||
// names (`read`, `bash`, ...) as the built-ins they replace.
|
||||
// Readonly sessions drop caller-supplied customTools — see comment above
|
||||
// about hermetic isolation. Only the wrapped read-only built-ins survive.
|
||||
const customToolList: ToolDefinition[] = [
|
||||
...(wrappedTools as ToolDefinition[]),
|
||||
...(isReadonly ? [] : (options.customTools ?? [])),
|
||||
...(options.customTools ?? []),
|
||||
];
|
||||
if (isReadonly && (options.customTools?.length ?? 0) > 0) {
|
||||
piLog.log(`readonly session — customTools (${options.customTools!.length}) skipped`);
|
||||
}
|
||||
// Last-chance abort hook. Fires *here* — after every awaited setup step
|
||||
// in createFnAgent (provider registration, worktree validation, resource
|
||||
// loader reload) and immediately before the actual LLM session spawn.
|
||||
|
||||
Reference in New Issue
Block a user