mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 09:31:39 +00:00
* fix(anthropic): forward Claude CLI auth profiles * fix(system-agent): inject CLI auth route stores * fix(claude-cli): pass profile credentials by descriptor * fix(anthropic): repair selected profile CI coverage * fix(anthropic): preserve profile owner validation * test(system-agent): preserve selected profile fixtures * test(system-agent): narrow selected profile fixture * test(system-agent): resolve profile store merge * fix(anthropic): forward profiles to node Claude runs * fix(system-agent): reconcile profile route projection * test(system-agent): thread profile store through projection * fix(anthropic): make selected profile authoritative * fix(system-agent): type auth setup failures * fix(system-agent): type setup auth failures * style: format Claude profile maintenance * fix(anthropic): keep gateway credentials off nodes * fix(anthropic): clear ambient auth for selected profiles * fix(anthropic): secure paired-node Claude auth * fix(node-host): type Claude fd spawn streams * style(node-host): satisfy Claude spawn lint * fix(process): capture exit before secret delivery * fix(anthropic): preserve node-native Claude auth
250 lines
8.2 KiB
TypeScript
250 lines
8.2 KiB
TypeScript
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;
|
|
};
|
|
|
|
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 source = Buffer.from(params.requestEnv?.[selected.requestEnv] ?? "", "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,
|
|
// The command-specific validator is the execution boundary. Approval sees
|
|
// every executable argument; prompt/stdin content remains request input.
|
|
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) => {
|
|
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,
|
|
});
|
|
} finally {
|
|
preparedSecret.cleanup();
|
|
}
|
|
return runResult;
|
|
},
|
|
runViaMacAppExecHost: params.deps.runViaMacAppExecHost,
|
|
// Agent runs already report through the agent-run stream. Suppress the
|
|
// system.run lifecycle side-channel, whose Gateway provenance is scoped
|
|
// exclusively to system.run invokes.
|
|
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,
|
|
});
|
|
}
|