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 () => {
|
it("logs createFnAgent startup diagnostics without leaking cwd", async () => {
|
||||||
const { piLog } = await import("../logger.js");
|
const { piLog } = await import("../logger.js");
|
||||||
const logSpy = vi.spyOn(piLog, "log").mockImplementation(() => {});
|
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
|
// `tools: "readonly"` MUST mean a hermetically sealed read-only session with
|
||||||
// way for the model to mutate state. Host extensions (`@runfusion/fusion`)
|
// respect to host extension injection. Host extensions (`@runfusion/fusion`)
|
||||||
// register write tools like `fn_task_create`, so they are deliberately
|
// can register write tools like `fn_task_create`, so they are deliberately
|
||||||
// EXCLUDED in readonly mode. Caller-supplied `customTools` are also dropped
|
// EXCLUDED in readonly mode. Caller-supplied `customTools` are preserved,
|
||||||
// for the same reason. Without this, summarizer/compaction sessions could
|
// since heartbeat/reviewer flows explicitly provide engine-owned tools.
|
||||||
// call write tools and mutate the task board (see FN-3057/FN-3058 incident).
|
// This keeps summarizer/compaction sessions safe while retaining intended
|
||||||
|
// delegation/memory tools for readonly engine sessions.
|
||||||
const isReadonly = options.tools === "readonly";
|
const isReadonly = options.tools === "readonly";
|
||||||
const effectiveExtensionPaths = isReadonly ? [] : hostExtensionPaths;
|
const effectiveExtensionPaths = isReadonly ? [] : hostExtensionPaths;
|
||||||
if (isReadonly && hostExtensionPaths.length > 0) {
|
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
|
// suppress the defaults with `noTools: "builtin"` and register our wrapped
|
||||||
// tools through `customTools` instead. The wrapped tools preserve the same
|
// tools through `customTools` instead. The wrapped tools preserve the same
|
||||||
// names (`read`, `bash`, ...) as the built-ins they replace.
|
// 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[] = [
|
const customToolList: ToolDefinition[] = [
|
||||||
...(wrappedTools as 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
|
// Last-chance abort hook. Fires *here* — after every awaited setup step
|
||||||
// in createFnAgent (provider registration, worktree validation, resource
|
// in createFnAgent (provider registration, worktree validation, resource
|
||||||
// loader reload) and immediately before the actual LLM session spawn.
|
// loader reload) and immediately before the actual LLM session spawn.
|
||||||
|
|||||||
Reference in New Issue
Block a user