openclaw/src/agents/pi-embedded-runner/run/tool-hook-wrapper.ts
gerald Ruby 8262a03060 feat: add GitHub 2FA gate extension for sensitive tools
Add a new extension that gates sensitive tool calls (exec, Bash, Write,
Edit, NotebookEdit) behind GitHub Device Flow authentication. Users must
approve on GitHub Mobile or enter a code at github.com/login/device
before the bot can execute dangerous operations.

Key changes:
- Wire up before_tool_call hook in tool execution path (tool-hook-wrapper.ts)
- Create 2fa-github extension with:
  - GitHub Device Authorization Flow implementation
  - File-based session store with TTL (~/.clawdbot/2fa-sessions.json)
  - Non-blocking flow: returns immediately with code, user retries after approval
  - Configurable tool list and session TTL (default 30 min)

Configuration:
  plugins.entries.2fa-github.config.clientId: "Ov23..."
  # or GITHUB_2FA_CLIENT_ID env var

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:32:13 -08:00

80 lines
2.5 KiB
TypeScript

/**
* Tool Hook Wrapper
*
* Wraps tool execute functions to invoke before_tool_call hooks before execution.
* If a hook returns { block: true }, the tool returns an error result instead of executing.
*/
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import type { AnyAgentTool } from "../../pi-tools.types.js";
import { log } from "../logger.js";
export type ToolHookContext = {
agentId?: string;
sessionKey?: string;
};
/**
* Create a blocked tool result with proper typing.
*/
function blockedResult(reason: string): AgentToolResult<unknown> {
return {
content: [{ type: "text", text: reason }],
details: { blocked: true, reason },
};
}
/**
* Wrap a tool with before_tool_call hook invocation.
* The hook can block execution or modify parameters.
*/
export function wrapToolWithHook(tool: AnyAgentTool, ctx: ToolHookContext): AnyAgentTool {
const originalExecute = tool.execute;
if (!originalExecute) return tool;
return {
...tool,
execute: async (toolCallId, params, signal, onUpdate) => {
const hookRunner = getGlobalHookRunner();
// Check if any before_tool_call hooks are registered
if (hookRunner?.hasHooks("before_tool_call")) {
try {
const hookResult = await hookRunner.runBeforeToolCall(
{ toolName: tool.name, params: params as Record<string, unknown> },
{ agentId: ctx.agentId, sessionKey: ctx.sessionKey, toolName: tool.name },
);
// If hook wants to block execution
if (hookResult?.block) {
log.debug(
`Tool ${tool.name} blocked by before_tool_call hook: ${hookResult.blockReason ?? "no reason given"}`,
);
return blockedResult(hookResult.blockReason ?? `Tool ${tool.name} blocked by plugin`);
}
// If hook modified params, use the modified version
if (hookResult?.params) {
params = hookResult.params;
}
} catch (err) {
log.warn(`before_tool_call hook failed for ${tool.name}: ${String(err)}`);
// Continue with execution on hook error (fail-open for safety)
}
}
// Execute the original tool
return originalExecute.call(tool, toolCallId, params, signal, onUpdate);
},
};
}
/**
* Wrap multiple tools with hook invocation.
*/
export function wrapToolsWithHook(tools: AnyAgentTool[], ctx: ToolHookContext): AnyAgentTool[] {
return tools.map((tool) => wrapToolWithHook(tool, ctx));
}