| import type { CloudflareAccessCredentials } from "../../packages/gateway-client/src/cloudflare-access.js"; |
| import type { DesktopHostConfig } from "../config/types.desktop.js"; |
| import { createExecApprovalPolicySnapshot } from "../infra/exec-approvals.js"; |
| import type { scanInstalledApps } from "../infra/installed-apps.js"; |
| import type { OpenClawPluginNodeHostCommandIo } from "../plugins/types.js"; |
| import type { OpenClawPluginNodeHostCommandContext } from "../plugins/types.node-host.js"; |
| import type { NodeHostClient } from "./client.js"; |
| import { |
| decodeClaudeCliNodeRunParams, |
| type ClaudeCliNodeRunParams, |
| type ClaudeCliNodeRunResult, |
| } from "./invoke-agent-cli-claude-params.js"; |
| import { runClaudeCliNodeCommand } from "./invoke-agent-cli-claude.js"; |
| import { |
| buildSystemRunApprovalPlan, |
| handleSystemRunInvoke, |
| resolveEffectiveSystemRunExecPolicy, |
| } from "./invoke-system-run.js"; |
| import type { NodeInvokeRequestPayload, RunResult, SkillBinsProvider } from "./invoke-types.js"; |
|
|
| export type NodeHostInvokeRuntime = { |
| claudePath?: string; |
| handleSystemRun?: typeof handleSystemRunInvoke; |
| signal?: AbortSignal; |
| pluginCommandIo?: OpenClawPluginNodeHostCommandIo; |
| pluginCommandContext?: OpenClawPluginNodeHostCommandContext; |
| installedAppsSharingEnabled?: boolean; |
| installedAppsPlatform?: NodeJS.Platform; |
| scanInstalledApps?: typeof scanInstalledApps; |
| gatewayUrl?: string; |
| gatewayTlsFingerprint?: string; |
| gatewayCloudflareAccess?: CloudflareAccessCredentials; |
| desktopHostConfig?: DesktopHostConfig; |
| emitProgress?: (text: string) => Promise<void>; |
| }; |
|
|
| type ClaudeCliNodeInvokeDeps = Pick< |
| Parameters<typeof handleSystemRunInvoke>[0], |
| | "resolveExecSecurity" |
| | "resolveExecAsk" |
| | "isCmdExeInvocation" |
| | "sanitizeEnv" |
| | "runViaMacAppExecHost" |
| | "buildExecEventPayload" |
| > & { |
| sendErrorResult: ( |
| client: NodeHostClient, |
| frame: NodeInvokeRequestPayload, |
| code: string, |
| message: string, |
| ) => Promise<void>; |
| sendInvalidRequestResult: ( |
| client: NodeHostClient, |
| frame: NodeInvokeRequestPayload, |
| error: unknown, |
| ) => Promise<void>; |
| sendInvokeResult: ( |
| client: NodeHostClient, |
| frame: NodeInvokeRequestPayload, |
| result: { |
| ok: boolean; |
| payload?: unknown; |
| payloadJSON?: string | null; |
| error?: { code?: string; message?: string } | null; |
| }, |
| ) => Promise<void>; |
| }; |
|
|
| const CLAUDE_NODE_AUTH_INPUTS = [ |
| { |
| requestEnv: "CLAUDE_CODE_OAUTH_TOKEN", |
| descriptorEnv: "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR", |
| }, |
| { |
| requestEnv: "ANTHROPIC_API_KEY", |
| descriptorEnv: "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR", |
| }, |
| ] as const; |
|
|
| function prepareClaudeNodeSecretInput(params: { |
| requestEnv: Record<string, string> | undefined; |
| childEnv: Record<string, string>; |
| }): { secretInput?: { fd: 3; createData: () => Buffer }; cleanup: () => void } { |
| const selected = CLAUDE_NODE_AUTH_INPUTS.find(({ requestEnv }) => |
| Object.hasOwn(params.requestEnv ?? {}, requestEnv), |
| ); |
| if (!selected) { |
| return { cleanup: () => {} }; |
| } |
| for (const key of [ |
| "ANTHROPIC_API_KEY", |
| "CLAUDE_CODE_OAUTH_TOKEN", |
| "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB", |
| ]) { |
| delete params.childEnv[key]; |
| } |
| const value = params.requestEnv?.[selected.requestEnv]?.trim(); |
| |
| if (!value) { |
| return { cleanup: () => {} }; |
| } |
| const source = Buffer.from(value, "utf8"); |
| params.childEnv[selected.descriptorEnv] = "3"; |
| return { |
| secretInput: { |
| fd: 3, |
| createData: () => Buffer.from(source), |
| }, |
| cleanup: () => source.fill(0), |
| }; |
| } |
|
|
| export async function handleClaudeCliNodeInvoke(params: { |
| frame: NodeInvokeRequestPayload; |
| client: NodeHostClient; |
| skillBins: SkillBinsProvider; |
| runtime: NodeHostInvokeRuntime; |
| deps: ClaudeCliNodeInvokeDeps; |
| }): Promise<void> { |
| if (!params.runtime.claudePath) { |
| await params.deps.sendErrorResult( |
| params.client, |
| params.frame, |
| "UNAVAILABLE", |
| "Claude CLI agent runs are unavailable", |
| ); |
| return; |
| } |
| const claudePath = params.runtime.claudePath; |
| let request: ClaudeCliNodeRunParams; |
| try { |
| request = await decodeClaudeCliNodeRunParams(params.frame.paramsJSON); |
| } catch (error) { |
| await params.deps.sendInvalidRequestResult(params.client, params.frame, error); |
| return; |
| } |
| const approvalCommand = [claudePath, ...request.argv]; |
| const preparedApproval = buildSystemRunApprovalPlan({ |
| command: approvalCommand, |
| ...(request.cwd ? { cwd: request.cwd } : {}), |
| ...(request.agentId ? { agentId: request.agentId } : {}), |
| ...(request.sessionKey ? { sessionKey: request.sessionKey } : {}), |
| }); |
| if (!preparedApproval.ok) { |
| await params.deps.sendErrorResult( |
| params.client, |
| params.frame, |
| "INVALID_REQUEST", |
| preparedApproval.message, |
| ); |
| return; |
| } |
| const { getRuntimeConfig: getNodeRuntimeConfig } = await import("../config/config.js"); |
| const execPolicy = await resolveEffectiveSystemRunExecPolicy({ |
| cfg: getNodeRuntimeConfig(), |
| agentId: request.agentId, |
| defaultSecurity: params.deps.resolveExecSecurity(undefined), |
| defaultAsk: params.deps.resolveExecAsk(undefined), |
| requireSocket: false, |
| }); |
| const approvalPlan = { |
| ...preparedApproval.plan, |
| policySnapshot: createExecApprovalPolicySnapshot({ |
| file: execPolicy.approvals.file, |
| agentId: request.agentId, |
| }), |
| }; |
| let runResult: RunResult | undefined; |
| await (params.runtime.handleSystemRun ?? handleSystemRunInvoke)({ |
| client: params.client, |
| |
| |
| |
| params: { |
| command: approvalCommand, |
| ...(request.cwd ? { cwd: request.cwd } : {}), |
| ...(request.env ? { env: request.env } : {}), |
| ...(request.agentId ? { agentId: request.agentId } : {}), |
| ...(request.sessionKey ? { sessionKey: request.sessionKey } : {}), |
| ...(request.systemRunPlan ? { systemRunPlan: request.systemRunPlan } : {}), |
| ...(request.approvalDecision ? { approvalDecision: request.approvalDecision } : {}), |
| timeoutMs: request.timeoutMs, |
| }, |
| skillBins: params.skillBins, |
| execHostEnforced: false, |
| execHostFallbackAllowed: true, |
| resolveExecSecurity: params.deps.resolveExecSecurity, |
| resolveExecAsk: params.deps.resolveExecAsk, |
| isCmdExeInvocation: params.deps.isCmdExeInvocation, |
| sanitizeEnv: params.deps.sanitizeEnv, |
| runCommand: async (approvalArgv, cwd, env, timeoutMs, _signal, assertCurrent) => { |
| const childEnv = { ...env }; |
| for (const key of request.clearEnv ?? []) { |
| if (!Object.hasOwn(request.env ?? {}, key)) { |
| delete childEnv[key]; |
| } |
| } |
| const preparedSecret = prepareClaudeNodeSecretInput({ |
| requestEnv: request.env, |
| childEnv, |
| }); |
| try { |
| runResult = await runClaudeCliNodeCommand({ |
| client: params.client, |
| frame: params.frame, |
| request, |
| argv: approvalArgv, |
| cwd, |
| env: childEnv, |
| secretInput: preparedSecret.secretInput, |
| timeoutMs, |
| signal: params.runtime.signal, |
| assertCurrent, |
| skillIo: params.runtime.pluginCommandIo, |
| }); |
| } finally { |
| preparedSecret.cleanup(); |
| } |
| return runResult; |
| }, |
| runViaMacAppExecHost: params.deps.runViaMacAppExecHost, |
| |
| |
| |
| sendNodeEvent: async () => {}, |
| buildExecEventPayload: params.deps.buildExecEventPayload, |
| sendInvokeResult: async (result) => { |
| if ( |
| !result.ok && |
| !request.approvalDecision && |
| result.error?.message?.includes("approval required") |
| ) { |
| await params.deps.sendInvokeResult(params.client, params.frame, { |
| ok: true, |
| payloadJSON: JSON.stringify({ |
| approvalRequired: true, |
| systemRunPlan: approvalPlan, |
| security: execPolicy.security, |
| ask: execPolicy.ask, |
| }), |
| }); |
| return; |
| } |
| if (!result.ok || !runResult) { |
| await params.deps.sendInvokeResult(params.client, params.frame, result); |
| return; |
| } |
| const payload: ClaudeCliNodeRunResult = { |
| exitCode: runResult.exitCode ?? 1, |
| stderrTail: runResult.stderr, |
| truncated: runResult.truncated, |
| ...(runResult.timedOut |
| ? { timeoutKind: runResult.noOutputTimedOut ? ("idle" as const) : ("hard" as const) } |
| : {}), |
| }; |
| await params.deps.sendInvokeResult(params.client, params.frame, { |
| ok: true, |
| payloadJSON: JSON.stringify(payload), |
| }); |
| }, |
| sendExecFinishedEvent: async () => {}, |
| preferMacAppExecHost: false, |
| }); |
| } |
|
|