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:
Jason (Json)
2026-07-21 23:27:37 -06:00
committed by GitHub
parent 13716ad4f4
commit 1a42e005fb
30 changed files with 1252 additions and 75 deletions
+53
View File
@@ -0,0 +1,53 @@
import type { ChildProcess } from "node:child_process";
import type { Writable } from "node:stream";
import type { SpawnSecretInput } from "./supervisor/types.js";
export type SpawnStdioEntry = "ignore" | "inherit" | "overlapped" | "pipe";
export function addSecretInputStdio(
stdio: SpawnStdioEntry[],
secretInput: SpawnSecretInput | undefined,
): void {
if (!secretInput) {
return;
}
if (!Number.isInteger(secretInput.fd) || secretInput.fd < 3) {
throw new Error("secret input file descriptor must be an integer greater than 2");
}
while (stdio.length <= secretInput.fd) {
stdio.push("ignore");
}
stdio[secretInput.fd] = process.platform === "win32" ? "overlapped" : "pipe";
}
export async function writeSecretInputToChild(
child: ChildProcess,
secretInput: SpawnSecretInput | undefined,
): Promise<void> {
if (!secretInput) {
return;
}
const stream = child.stdio[secretInput.fd] as Writable | null | undefined;
if (!stream || typeof stream.end !== "function") {
throw new Error(`secret input file descriptor ${secretInput.fd} is unavailable`);
}
let data: Buffer | undefined;
try {
data = secretInput.createData();
// End the parent pipe immediately after delivery so descendants cannot
// inherit a still-readable credential stream.
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
stream.off("error", onError);
reject(error);
};
stream.once("error", onError);
stream.end(data, () => {
stream.off("error", onError);
resolve();
});
});
} finally {
data?.fill(0);
}
}
+93 -1
View File
@@ -4,7 +4,7 @@ import { EventEmitter } from "node:events";
import fs from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { PassThrough } from "node:stream";
import { PassThrough, Writable } from "node:stream";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../../test/helpers/temp-dir.js";
import {
@@ -48,6 +48,10 @@ function createStubChild(pid = 1234) {
child.stdin = new PassThrough() as ChildProcess["stdin"];
child.stdout = new PassThrough() as ChildProcess["stdout"];
child.stderr = new PassThrough() as ChildProcess["stderr"];
Object.defineProperty(child, "stdio", {
value: [child.stdin, child.stdout, child.stderr],
configurable: true,
});
Object.defineProperty(child, "pid", { value: pid, configurable: true });
Object.defineProperty(child, "killed", { value: false, configurable: true, writable: true });
Object.defineProperty(child, "exitCode", { value: null, configurable: true, writable: true });
@@ -219,6 +223,94 @@ describe("createChildAdapter", () => {
expect(killMock).toHaveBeenCalledWith("SIGKILL");
});
it("writes secret input to an extra descriptor and zeroes the transient buffer", async () => {
const { child } = createStubChild();
const secretStream = new PassThrough();
const chunks: Buffer[] = [];
secretStream.on("data", (chunk: Buffer) => {
chunks.push(Buffer.from(chunk));
});
Object.defineProperty(child, "stdio", {
value: [child.stdin, child.stdout, child.stderr, secretStream],
configurable: true,
});
spawnWithFallbackMock.mockResolvedValue({
child,
usedFallback: false,
});
const transient = Buffer.from("selected-secret", "utf8");
await createChildAdapter({
argv: ["claude", "-p"],
stdinMode: "pipe-open",
secretInput: {
fd: 3,
createData: () => transient,
},
});
expect(firstSpawnWithFallbackParams().options?.stdio).toEqual([
"pipe",
"pipe",
"pipe",
process.platform === "win32" ? "overlapped" : "pipe",
]);
expect(Buffer.concat(chunks).toString("utf8")).toBe("selected-secret");
expect(transient.equals(Buffer.alloc(transient.length))).toBe(true);
});
it("captures child close while secret input delivery is still pending", async () => {
const { child, emitClose } = createStubChild();
const secretStream = new Writable({
write(_chunk, _encoding, callback) {
emitClose(0);
setImmediate(callback);
},
});
Object.defineProperty(child, "stdio", {
value: [child.stdin, child.stdout, child.stderr, secretStream],
configurable: true,
});
spawnWithFallbackMock.mockResolvedValue({
child,
usedFallback: false,
});
const adapter = await createChildAdapter({
argv: ["claude", "-p"],
stdinMode: "pipe-open",
secretInput: {
fd: 3,
createData: () => Buffer.from("selected-secret"),
},
});
await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: null });
});
it("uses overlapped I/O for a Windows secret descriptor", async () => {
setPlatform("win32");
const { child } = createStubChild();
Object.defineProperty(child, "stdio", {
value: [child.stdin, child.stdout, child.stderr, new PassThrough()],
configurable: true,
});
spawnWithFallbackMock.mockResolvedValue({
child,
usedFallback: false,
});
await createChildAdapter({
argv: ["claude.exe", "-p"],
secretInput: {
fd: 3,
createData: () => Buffer.from("secret"),
},
});
expect(firstSpawnWithFallbackParams().options?.stdio?.[3]).toBe("overlapped");
});
it("passes detached:false to signalProcessTree when spawn fell back to no-detach (#71662 follow-up)", async () => {
// Simulate the fallback scenario: spawnWithFallback retried with
// detached:false because the initial detached spawn failed. The kill
+20 -7
View File
@@ -8,6 +8,11 @@ import {
} from "../../../plugin-sdk/windows-spawn.js";
import { signalProcessTree } from "../../kill-tree.js";
import { prepareOomScoreAdjustedSpawn } from "../../linux-oom-score.js";
import {
addSecretInputStdio,
type SpawnStdioEntry,
writeSecretInputToChild,
} from "../../spawn-secret-input.js";
import { spawnWithFallback } from "../../spawn-utils.js";
import {
buildWindowsCmdExeCommandLine,
@@ -15,7 +20,7 @@ import {
resolveTrustedWindowsCmdExe,
resolveWindowsCommandShim,
} from "../../windows-command.js";
import type { ManagedRunStdin, SpawnProcessAdapter } from "../types.js";
import type { ManagedRunStdin, SpawnProcessAdapter, SpawnSecretInput } from "../types.js";
import { toStringEnv } from "./env.js";
const FORCE_KILL_WAIT_FALLBACK_MS = 4000;
@@ -78,6 +83,7 @@ export async function createChildAdapter(params: {
windowsVerbatimArguments?: boolean;
input?: string;
stdinMode?: "inherit" | "pipe-open" | "pipe-closed";
secretInput?: SpawnSecretInput;
}): Promise<ChildAdapter> {
const baseEnv = params.env ? toStringEnv(params.env) : undefined;
const invocation = resolveChildInvocation({
@@ -96,19 +102,17 @@ export async function createChildAdapter(params: {
// existing POSIX detached behavior.
const useDetached = process.platform !== "win32" && !isServiceManagedRuntime();
const stdio: SpawnStdioEntry[] = [stdinMode === "inherit" ? "inherit" : "pipe", "pipe", "pipe"];
addSecretInputStdio(stdio, params.secretInput);
const options: SpawnOptions = {
cwd: params.cwd,
env: preparedSpawn.env,
stdio: ["pipe", "pipe", "pipe"],
stdio,
detached: useDetached,
windowsHide: true,
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
};
if (stdinMode === "inherit") {
options.stdio = ["inherit", "pipe", "pipe"];
} else {
options.stdio = ["pipe", "pipe", "pipe"];
}
const spawned = await spawnWithFallback({
argv: [preparedSpawn.command, ...preparedSpawn.args],
@@ -397,6 +401,15 @@ export async function createChildAdapter(params: {
settleWait(resolveObservedExitState(childCloseState));
});
if (params.secretInput) {
try {
await writeSecretInputToChild(spawned.child, params.secretInput);
} catch (error) {
spawned.child.kill("SIGKILL");
throw error;
}
}
const wait = async () => {
if (waitResult) {
return waitResult;
+20
View File
@@ -140,6 +140,26 @@ describe("process supervisor", () => {
expect(adapter.disposeMock).toHaveBeenCalledTimes(1);
});
it("passes private secret input to the child adapter", async () => {
const adapter = createStubChildAdapter();
createChildAdapterMock.mockResolvedValue(adapter);
const secretInput = {
fd: 3,
createData: () => Buffer.from("secret"),
};
const supervisor = createProcessSupervisor();
const run = await spawnChild(supervisor, {
sessionId: "s1",
argv: createWriteStdoutArgv("ok"),
secretInput,
});
adapter.settle(0);
await run.wait();
expect(createChildAdapterMock).toHaveBeenCalledWith(expect.objectContaining({ secretInput }));
});
it("enforces no-output timeout for silent processes", async () => {
vi.useFakeTimers();
const adapter = createStubChildAdapter({
+1
View File
@@ -211,6 +211,7 @@ export function createProcessSupervisor(): ProcessSupervisor {
windowsVerbatimArguments: input.windowsVerbatimArguments,
input: input.input,
stdinMode: input.stdinMode,
secretInput: input.secretInput,
});
registry.updateState(runId, "running", { pid: adapter.pid });
+6
View File
@@ -58,6 +58,11 @@ export type ManagedRunStdin = {
writableFinished?: boolean;
};
export type SpawnSecretInput = {
fd: number;
createData: () => Buffer;
};
export type SpawnProcessAdapter<WaitSignal = NodeJS.Signals | number | null> = {
pid?: number;
stdin?: ManagedRunStdin;
@@ -97,6 +102,7 @@ type SpawnChildInput = SpawnBaseInput & {
windowsVerbatimArguments?: boolean;
input?: string;
stdinMode?: "inherit" | "pipe-open" | "pipe-closed";
secretInput?: SpawnSecretInput;
};
type SpawnPtyInput = SpawnBaseInput & {