Files
fusion/packages/engine/src/workflow-step-tool-policy.ts
gsxdsm 3e0391eada FN-7267: expose MCP tools to planning sessions
Expose configured MCP session tools to read-only planning and mission lanes while keeping other read-only sessions locked down.

- Add an explicit allowMcpToolsInReadonly option to createFnAgent and apply it only to connected MCP tools.
- Opt planning, streaming planning, mission, milestone, and slice interview sessions into read-only MCP tools.
- Expand regression coverage for MCP forwarding, read-only filtering, and session tool disposal behavior.
- Document the read-only MCP opt-in model and add a published package changeset.

Files changed:
 .changeset/fn-7267-planning-mission-mcp.md         |  7 +++
 docs/mcp.md                                        |  4 +-
 .../src/__tests__/mcp-lane-forwarding.test.ts      |  4 ++
 .../dashboard/src/milestone-slice-interview.ts     |  4 ++
 packages/dashboard/src/mission-interview.ts        |  4 ++
 packages/dashboard/src/planning.ts                 |  4 ++
 .../src/__tests__/mcp-surface-coverage.test.ts     | 25 +++++++-
 .../src/__tests__/pi-create-fn-agent.test.ts       | 67 ++++++++++++++++++++++
 .../src/__tests__/pi-mcp-session-tools.test.ts     |  6 +-
 .../workflow-step-readonly-allowlist.test.ts       | 17 +++++-
 packages/engine/src/pi.ts                          | 17 +++++-
 packages/engine/src/workflow-step-tool-policy.ts   | 15 ++++-
 12 files changed, 163 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7267

Fusion-Task-Lineage: 8d9cac1a-edcc-453d-bda1-9abb58de7f86

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-30 00:09:36 -07:00

79 lines
2.4 KiB
TypeScript

import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
import { TASK_AGENT_MUTATION_TOOLS } from "./gating-classifications.js";
export const READONLY_ALLOWLIST = [
"read",
"grep",
"find",
"ls",
"WebSearch",
"WebFetch",
"fn_web_fetch",
"fn_task_show",
"fn_task_list",
"fn_insight_list",
"fn_insight_show",
"fn_list_agents",
"fn_get_agent_config",
] as const;
const WRITE_BUILTIN_TOOLS = ["edit", "write", "bash"] as const;
export const DENIED_IN_READONLY = [
...WRITE_BUILTIN_TOOLS,
...Array.from(TASK_AGENT_MUTATION_TOOLS).sort(),
] as const;
const READONLY_ALLOWLIST_SET = new Set<string>(READONLY_ALLOWLIST);
const DENIED_IN_READONLY_SET = new Set<string>(DENIED_IN_READONLY);
// Note: fn_task_browse_github_issues is read-only by behavior, but readonly sessions
// intentionally exclude host extensions in pi.ts, so it remains absent by default.
export class ReadonlyViolationError extends Error {
readonly code = "READONLY_VIOLATION" as const;
constructor(
public readonly taskId: string,
public readonly stepName: string,
public readonly toolName: string,
) {
super(`[readonly-violation] ${stepName} attempted to use denied tool "${toolName}" for task ${taskId}`);
this.name = "ReadonlyViolationError";
}
}
export function isReadonlyAllowed(toolName: string): boolean {
return READONLY_ALLOWLIST_SET.has(toolName.trim());
}
export interface ReadonlyCustomToolFilterOptions {
/**
* FNXC:McpConfig 2026-06-29-00:00:
* Planning and mission interviews use read-only sessions but intentionally opt into MCP session tools after Fusion has connected, namespaced, and materialized those tools. Keep this as a per-tool predicate instead of a blanket `mcp__` name allowlist so other read-only lanes and caller-supplied custom tools remain protected by default.
*/
allowTool?: (tool: ToolDefinition) => boolean;
}
export function filterCustomToolsForReadonly(
tools: ToolDefinition[],
options: ReadonlyCustomToolFilterOptions = {},
): { allowed: ToolDefinition[]; denied: string[] } {
const allowed: ToolDefinition[] = [];
const denied: string[] = [];
for (const tool of tools) {
const name = tool.name?.trim() ?? "";
if (!name) continue;
if (isReadonlyAllowed(name) || options.allowTool?.(tool) === true) {
allowed.push(tool);
continue;
}
if (DENIED_IN_READONLY_SET.has(name)) {
denied.push(name);
}
}
return { allowed, denied };
}