mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(anthropic): forward selected profiles to Claude CLI (#112458)
* 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
This commit is contained in:
@@ -59,6 +59,45 @@ type ClaudeCliNodeInvokeDeps = Pick<
|
||||
) => 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;
|
||||
@@ -137,16 +176,31 @@ export async function handleClaudeCliNodeInvoke(params: {
|
||||
isCmdExeInvocation: params.deps.isCmdExeInvocation,
|
||||
sanitizeEnv: params.deps.sanitizeEnv,
|
||||
runCommand: async (approvalArgv, cwd, env, timeoutMs) => {
|
||||
runResult = await runClaudeCliNodeCommand({
|
||||
client: params.client,
|
||||
frame: params.frame,
|
||||
request,
|
||||
argv: approvalArgv,
|
||||
cwd,
|
||||
env,
|
||||
timeoutMs,
|
||||
signal: params.runtime.signal,
|
||||
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,
|
||||
|
||||
@@ -50,13 +50,65 @@ const VALUE_ARGS = new Set([
|
||||
"--disallowedTools",
|
||||
]);
|
||||
|
||||
const ENV_ALLOWLIST = new Set(["FORCE_COLOR", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "TERM"]);
|
||||
const ENV_ALLOWLIST = new Set([
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"FORCE_COLOR",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"NO_COLOR",
|
||||
"TERM",
|
||||
]);
|
||||
const CLEAR_ENV_ALLOWLIST = new Set([
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_API_KEY_OLD",
|
||||
"ANTHROPIC_API_TOKEN",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_BASE_URL",
|
||||
"ANTHROPIC_CUSTOM_HEADERS",
|
||||
"ANTHROPIC_OAUTH_TOKEN",
|
||||
"ANTHROPIC_UNIX_SOCKET",
|
||||
"CLAUDE_CONFIG_DIR",
|
||||
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
||||
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
|
||||
"CLAUDE_CODE_ENTRYPOINT",
|
||||
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
||||
"CLAUDE_CODE_OAUTH_SCOPES",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
||||
"CLAUDE_CODE_PLUGIN_CACHE_DIR",
|
||||
"CLAUDE_CODE_PLUGIN_SEED_DIR",
|
||||
"CLAUDE_CODE_REMOTE",
|
||||
"CLAUDE_CODE_USE_COWORK_PLUGINS",
|
||||
"CLAUDE_CODE_USE_BEDROCK",
|
||||
"CLAUDE_CODE_USE_FOUNDRY",
|
||||
"CLAUDE_CODE_USE_VERTEX",
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_HEADERS",
|
||||
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
|
||||
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
|
||||
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
|
||||
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
|
||||
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
||||
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
|
||||
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
|
||||
"OTEL_LOGS_EXPORTER",
|
||||
"OTEL_METRICS_EXPORTER",
|
||||
"OTEL_SDK_DISABLED",
|
||||
"OTEL_TRACES_EXPORTER",
|
||||
]);
|
||||
|
||||
export type ClaudeCliNodeRunParams = {
|
||||
argv: string[];
|
||||
stdin?: string;
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
clearEnv?: string[];
|
||||
systemPrompt?: string;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
@@ -172,6 +224,7 @@ export async function decodeClaudeCliNodeRunParams(
|
||||
"stdin",
|
||||
"cwd",
|
||||
"env",
|
||||
"clearEnv",
|
||||
"systemPrompt",
|
||||
"agentId",
|
||||
"sessionKey",
|
||||
@@ -234,6 +287,25 @@ export async function decodeClaudeCliNodeRunParams(
|
||||
}
|
||||
env[key] = requireBoundedString(candidate, `env.${key}`, MAX_ARG_BYTES);
|
||||
}
|
||||
if (Object.hasOwn(env, "ANTHROPIC_API_KEY") && Object.hasOwn(env, "CLAUDE_CODE_OAUTH_TOKEN")) {
|
||||
throw new Error("INVALID_REQUEST: exactly one Claude credential may be provided");
|
||||
}
|
||||
}
|
||||
let clearEnv: string[] | undefined;
|
||||
if (value.clearEnv !== undefined) {
|
||||
if (!Array.isArray(value.clearEnv) || value.clearEnv.length > CLEAR_ENV_ALLOWLIST.size) {
|
||||
throw new Error("INVALID_REQUEST: clearEnv must be a bounded array");
|
||||
}
|
||||
clearEnv = [];
|
||||
for (const candidate of value.clearEnv) {
|
||||
const key = requireBoundedString(candidate, "clearEnv entry", MAX_ARG_BYTES);
|
||||
if (!CLEAR_ENV_ALLOWLIST.has(key)) {
|
||||
throw new Error(`INVALID_REQUEST: clearEnv key is not allowed: ${key}`);
|
||||
}
|
||||
if (!clearEnv.includes(key)) {
|
||||
clearEnv.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
argv,
|
||||
@@ -245,6 +317,7 @@ export async function decodeClaudeCliNodeRunParams(
|
||||
...(systemRunPlan ? { systemRunPlan: systemRunPlan as SystemRunApprovalPlan } : {}),
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(env ? { env } : {}),
|
||||
...(clearEnv ? { clearEnv } : {}),
|
||||
idleTimeoutMs: validateTimeout(
|
||||
value.idleTimeoutMs,
|
||||
"idleTimeoutMs",
|
||||
|
||||
@@ -84,7 +84,8 @@ describe("Claude CLI node command", () => {
|
||||
stdin: "hello",
|
||||
systemPrompt: "private prompt",
|
||||
cwd,
|
||||
env: { NO_COLOR: "1" },
|
||||
env: { NO_COLOR: "1", CLAUDE_CODE_OAUTH_TOKEN: "selected-node-token" },
|
||||
clearEnv: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 2_000,
|
||||
}),
|
||||
@@ -93,7 +94,8 @@ describe("Claude CLI node command", () => {
|
||||
cwd,
|
||||
stdin: "hello",
|
||||
systemPrompt: "private prompt",
|
||||
env: { NO_COLOR: "1" },
|
||||
env: { NO_COLOR: "1", CLAUDE_CODE_OAUTH_TOKEN: "selected-node-token" },
|
||||
clearEnv: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +120,39 @@ describe("Claude CLI node command", () => {
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("environment key is not allowed");
|
||||
await expect(
|
||||
decodeClaudeCliNodeRunParams(
|
||||
JSON.stringify({
|
||||
argv: ["-p"],
|
||||
clearEnv: [["OPENCLAW", "GATEWAY", "TOKEN"].join("_")],
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 2_000,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("clearEnv key is not allowed");
|
||||
await expect(
|
||||
decodeClaudeCliNodeRunParams(
|
||||
JSON.stringify({
|
||||
argv: ["-p"],
|
||||
env: { CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1" },
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 2_000,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("environment key is not allowed");
|
||||
await expect(
|
||||
decodeClaudeCliNodeRunParams(
|
||||
JSON.stringify({
|
||||
argv: ["-p"],
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: "selected-api-key",
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "selected-oauth-token",
|
||||
},
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 2_000,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("exactly one Claude credential");
|
||||
});
|
||||
|
||||
it("requires binary availability before consulting exec approval policy", async () => {
|
||||
@@ -191,6 +226,128 @@ describe("Claude CLI node command", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("converts forwarded OAuth into a child-only descriptor after approval", async () => {
|
||||
const executable = await executableScript(`
|
||||
const fs = require("node:fs");
|
||||
const secret = fs.readFileSync(3, "utf8");
|
||||
process.stdout.write(JSON.stringify({
|
||||
type: "result",
|
||||
result: secret,
|
||||
descriptor: process.env.CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR,
|
||||
rawPresent: Object.hasOwn(process.env, "CLAUDE_CODE_OAUTH_TOKEN"),
|
||||
scrubPresent: Object.hasOwn(process.env, "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"),
|
||||
}) + "\\n");`);
|
||||
const calls: Array<{ method: string; params: unknown }> = [];
|
||||
const handleSystemRun = vi.fn(
|
||||
async (options: {
|
||||
params: { command: string[]; env?: Record<string, string>; timeoutMs?: number };
|
||||
runCommand: (
|
||||
argv: string[],
|
||||
cwd: string | undefined,
|
||||
env: Record<string, string> | undefined,
|
||||
timeoutMs: number | undefined,
|
||||
) => Promise<unknown>;
|
||||
sendInvokeResult: (result: unknown) => Promise<void>;
|
||||
}) => {
|
||||
await options.runCommand(
|
||||
options.params.command,
|
||||
undefined,
|
||||
{
|
||||
...process.env,
|
||||
...options.params.env,
|
||||
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
|
||||
} as Record<string, string>,
|
||||
options.params.timeoutMs,
|
||||
);
|
||||
await options.sendInvokeResult({ ok: true });
|
||||
},
|
||||
);
|
||||
await handleInvoke(
|
||||
frame({
|
||||
argv: ["-p"],
|
||||
env: { CLAUDE_CODE_OAUTH_TOKEN: "selected-node-oauth" },
|
||||
clearEnv: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 5_000,
|
||||
}),
|
||||
client(calls),
|
||||
{ current: async () => [] },
|
||||
undefined,
|
||||
{ claudePath: executable, handleSystemRun: handleSystemRun as never },
|
||||
);
|
||||
|
||||
const progress = calls
|
||||
.filter((call) => call.method === "node.invoke.progress")
|
||||
.map((call) => (call.params as { chunk: string }).chunk)
|
||||
.join("");
|
||||
expect(progress).toContain('"result":"selected-node-oauth"');
|
||||
expect(progress).toContain('"descriptor":"3"');
|
||||
expect(progress).toContain('"rawPresent":false');
|
||||
expect(progress).toContain('"scrubPresent":false');
|
||||
expect(calls).toContainEqual({
|
||||
method: "node.invoke.result",
|
||||
params: expect.objectContaining({
|
||||
ok: true,
|
||||
payloadJSON: expect.stringContaining('"exitCode":0'),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves node-native Claude auth when no profile credential is forwarded", async () => {
|
||||
const executable = await executableScript(`
|
||||
process.stdout.write(JSON.stringify({
|
||||
type: "result",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
oauth: process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
||||
scrub: process.env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB,
|
||||
}) + "\\n");`);
|
||||
const calls: Array<{ method: string; params: unknown }> = [];
|
||||
const handleSystemRun = vi.fn(
|
||||
async (options: {
|
||||
params: { command: string[]; timeoutMs?: number };
|
||||
runCommand: (
|
||||
argv: string[],
|
||||
cwd: string | undefined,
|
||||
env: Record<string, string> | undefined,
|
||||
timeoutMs: number | undefined,
|
||||
) => Promise<unknown>;
|
||||
sendInvokeResult: (result: unknown) => Promise<void>;
|
||||
}) => {
|
||||
await options.runCommand(
|
||||
options.params.command,
|
||||
undefined,
|
||||
{
|
||||
...process.env,
|
||||
ANTHROPIC_API_KEY: "node-native-api-key",
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "node-native-oauth",
|
||||
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
|
||||
} as Record<string, string>,
|
||||
options.params.timeoutMs,
|
||||
);
|
||||
await options.sendInvokeResult({ ok: true });
|
||||
},
|
||||
);
|
||||
await handleInvoke(
|
||||
frame({
|
||||
argv: ["-p"],
|
||||
idleTimeoutMs: 1_000,
|
||||
timeoutMs: 5_000,
|
||||
}),
|
||||
client(calls),
|
||||
{ current: async () => [] },
|
||||
undefined,
|
||||
{ claudePath: executable, handleSystemRun: handleSystemRun as never },
|
||||
);
|
||||
|
||||
const progress = calls
|
||||
.filter((call) => call.method === "node.invoke.progress")
|
||||
.map((call) => (call.params as { chunk: string }).chunk)
|
||||
.join("");
|
||||
expect(progress).toContain('"apiKey":"node-native-api-key"');
|
||||
expect(progress).toContain('"oauth":"node-native-oauth"');
|
||||
expect(progress).toContain('"scrub":"1"');
|
||||
});
|
||||
|
||||
it("streams stdin-driven stdout and cleans up a node-local system prompt file", async () => {
|
||||
const executable = await executableScript(`
|
||||
const fs = require("node:fs");
|
||||
@@ -236,6 +393,59 @@ process.stdin.on("end", () => {
|
||||
await expect(fs.stat(promptPath ?? "")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
descriptorEnv: "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
||||
rawEnv: "CLAUDE_CODE_OAUTH_TOKEN",
|
||||
},
|
||||
{
|
||||
descriptorEnv: "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
|
||||
rawEnv: "ANTHROPIC_API_KEY",
|
||||
},
|
||||
])(
|
||||
"delivers selected credentials through fd 3 for $rawEnv",
|
||||
async ({ descriptorEnv, rawEnv }) => {
|
||||
const executable = await executableScript(`
|
||||
const fs = require("node:fs");
|
||||
const secret = fs.readFileSync(3, "utf8");
|
||||
process.stdout.write(JSON.stringify({
|
||||
type: "result",
|
||||
result: secret,
|
||||
descriptor: process.env[${JSON.stringify(descriptorEnv)}],
|
||||
rawPresent: Object.hasOwn(process.env, ${JSON.stringify(rawEnv)}),
|
||||
scrubPresent: Object.hasOwn(process.env, "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"),
|
||||
}) + "\\n");`);
|
||||
const request = { argv: ["-p"], idleTimeoutMs: 1_000, timeoutMs: 5_000 };
|
||||
const calls: Array<{ method: string; params: unknown }> = [];
|
||||
const result = await runClaudeCliNodeCommand({
|
||||
client: client(calls),
|
||||
frame: frame(request),
|
||||
request,
|
||||
argv: [executable, ...request.argv],
|
||||
cwd: undefined,
|
||||
env: {
|
||||
...process.env,
|
||||
[descriptorEnv]: "3",
|
||||
} as Record<string, string>,
|
||||
secretInput: {
|
||||
fd: 3,
|
||||
createData: () => Buffer.from("selected-node-secret"),
|
||||
},
|
||||
timeoutMs: request.timeoutMs,
|
||||
});
|
||||
|
||||
const progress = calls
|
||||
.filter((call) => call.method === "node.invoke.progress")
|
||||
.map((call) => (call.params as { chunk: string }).chunk)
|
||||
.join("");
|
||||
expect(progress).toContain('"result":"selected-node-secret"');
|
||||
expect(progress).toContain('"descriptor":"3"');
|
||||
expect(progress).toContain('"rawPresent":false');
|
||||
expect(progress).toContain('"scrubPresent":false');
|
||||
expect(result).toMatchObject({ exitCode: 0, success: true });
|
||||
},
|
||||
);
|
||||
|
||||
it("caps streamed output consistently with system.run", async () => {
|
||||
const executable = await executableScript(
|
||||
`let writes = 0;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/** Validates and streams one approval-gated Claude CLI turn on a headless node. */
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { signalProcessTree } from "../process/kill-tree.js";
|
||||
import {
|
||||
addSecretInputStdio,
|
||||
type SpawnStdioEntry,
|
||||
writeSecretInputToChild,
|
||||
} from "../process/spawn-secret-input.js";
|
||||
import type { SpawnSecretInput } from "../process/supervisor/types.js";
|
||||
import { resolveSafeChildProcessInvocation } from "../process/windows-command.js";
|
||||
import { truncateUtf8Suffix } from "../utils/utf8-truncate.js";
|
||||
import type { NodeHostClient } from "./client.js";
|
||||
@@ -33,6 +39,7 @@ export async function runClaudeCliNodeCommand(params: {
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
env: Record<string, string> | undefined;
|
||||
secretInput?: SpawnSecretInput;
|
||||
timeoutMs: number | undefined;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<RunResult> {
|
||||
@@ -79,14 +86,16 @@ export async function runClaudeCliNodeCommand(params: {
|
||||
cwd: params.cwd,
|
||||
env: params.env ?? process.env,
|
||||
});
|
||||
const stdio: SpawnStdioEntry[] = ["pipe", "pipe", "pipe"];
|
||||
addSecretInputStdio(stdio, params.secretInput);
|
||||
const child = spawn(invocation.command, invocation.args, {
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
stdio,
|
||||
...(process.platform !== "win32" ? { detached: true } : {}),
|
||||
windowsHide: invocation.windowsHide,
|
||||
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
||||
});
|
||||
}) as ChildProcessWithoutNullStreams;
|
||||
|
||||
const kill = () => {
|
||||
const pid = child.pid;
|
||||
@@ -252,6 +261,10 @@ export async function runClaudeCliNodeCommand(params: {
|
||||
truncated,
|
||||
});
|
||||
};
|
||||
void writeSecretInputToChild(child, params.secretInput).catch((error: unknown) => {
|
||||
kill();
|
||||
void finish(null, error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
child.once("error", (error) => void finish(null, error));
|
||||
child.once("close", (code) => void finish(code));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user