Files
fusion/packages/pi-claude-cli/src/control-handler.ts
Fusion f25aad9ffb feat(FN-2938): merge fusion/fn-2938
- fix(FN-2938): satisfy lint for control-request debug logging
- test(FN-2938): complete Step 4 — update protocol regression tests
- feat(FN-2938): complete Step 3 — remove stdin control request routing
- feat(FN-2938): complete Step 2 — make control handler pure
- feat(FN-2938): complete Step 1 — fix stdin EOF and spawn flags
- feat(FN-2929): merge fusion/fn-2929

Fusion-Task-Id: FN-2938
2026-04-29 02:07:35 -07:00

83 lines
2.2 KiB
TypeScript

/**
* Control protocol handler for Claude CLI stream-json communication.
*
* Processes control_request messages from Claude CLI stdout and returns a
* control_response decision object.
*
* - Custom MCP tools (mcp__custom-tools__*): DENIED — pi executes these
* - Everything else (user MCP tools, internal tools): ALLOWED — Claude handles
*/
import type { ClaudeControlRequest } from "./types";
import { CUSTOM_TOOLS_MCP_PREFIX } from "./tool-mapping.js";
export const TOOL_EXECUTION_DENIED_MESSAGE =
"Tool execution is unavailable in this environment.";
/** Prefix for MCP (Model Context Protocol) tool names. */
export const MCP_PREFIX = "mcp__";
interface ControlResponse {
type: "control_response";
request_id: string;
response: {
subtype: "success";
response: {
behavior: "allow" | "deny";
message?: string;
};
};
}
/**
* Handle a control_request from the Claude CLI.
*
* Denies custom MCP tools (mcp__custom-tools__*) so pi can execute them.
* Allows everything else (user MCP tools, internal Claude tools).
*
* Pure function: no side effects and no stdin writes.
*
* @returns Decision payload with allow/deny result and serialized response object
*/
export function handleControlRequest(
msg: ClaudeControlRequest,
): { allowed: boolean; response: ControlResponse } {
if (!msg.request_id || !msg.request) {
console.error(
"[pi-claude-cli] Malformed control_request: missing request_id or request object",
msg,
);
return {
allowed: false,
response: {
type: "control_response",
request_id: msg.request_id ?? "",
response: {
subtype: "success",
response: {
behavior: "deny",
message: TOOL_EXECUTION_DENIED_MESSAGE,
},
},
},
};
}
const toolName = msg.request?.tool_name ?? "";
const isCustomTool = toolName.startsWith(CUSTOM_TOOLS_MCP_PREFIX);
const response: ControlResponse = {
type: "control_response",
request_id: msg.request_id,
response: {
subtype: "success",
response: isCustomTool
? { behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }
: { behavior: "allow" },
},
};
return { allowed: !isCustomTool, response };
}