fix(acpx): reject invalid credential environment callbacks

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-23 06:56:13 -07:00
1 parent 9a0ac12318
commit d5905a5489
3 files changed
+42 -17

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ COPY cli/package.json ./cli/package.json
# The complete resolved lock (including transitive integrity hashes) is reviewed.
# Reject registry-time drift BEFORE installing packages or running lifecycle code.
# Refresh this digest together with source/provider dependency changes.
ARG PAPERCLIP_RUNNER_LOCK_SHA256=a297efff4b2dc0db5d8680793c1194efd2d8d3643f6935ab40d659b3fa34542e
ARG PAPERCLIP_RUNNER_LOCK_SHA256=0c06ab38c1d4b6b58e1c9f5ee85eb79f97cecd83e751b5d27c3b262faf5ab265
RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \
&& sha256sum -c /tmp/provider-lock.sha256 \
&& pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...'
@@ -178,6 +178,28 @@ test("the ACPX patch fails closed on an invalid spawn environment", () => {
);
});
test("authentication rejects invalid isolated environments without host fallback", () => {
const addedSource = acpxPatch.split("\n")
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
.map((line) => line.slice(1)).join("\n");
const start = addedSource.indexOf("function isPlainStringEnvironment(value)");
const end = addedSource.indexOf("function buildAgentEnvironment(", start);
assert.ok(start >= 0 && end > start);
const hostEnvironment = { XAI_API_KEY: "host-credential-must-not-leak" };
const resolveEnvironment = new Function(
"process", `${addedSource.slice(start, end)}; return resolveAgentEnvironment;`,
)({ env: hostEnvironment });
for (const invalid of [undefined, null, [], "invalid", { XAI_API_KEY: 1 }]) {
assert.throws(() => resolveEnvironment(() => invalid), TypeError);
}
const isolated = {};
assert.equal(resolveEnvironment(() => isolated), isolated);
assert.equal(resolveEnvironment(() => isolated).XAI_API_KEY, undefined);
assert.equal(resolveEnvironment(undefined), hostEnvironment);
assert.match(addedSource, /readEnvCredential\(method\.id, resolveAgentEnvironment\(this\.options\.spawnEnvironment\)\)/);
assert.match(addedSource, /resolveAgentEnvironment\(this\.options\.spawnEnvironment\)\)\.XAI_API_KEY/);
});
test("the Codex patch enforces isolated instructions, tools, and skills", () => {
for (const token of [
"PAPERCLIP_ACPX_ISOLATED_CONTEXT",
+19 -16
View File
@@ -52,7 +52,7 @@ index d454fd7..991d21d 100644
if (typeof value === "string" && value.trim().length > 0) return value;
}
function protectedEnvKey(key) {
@@ -3135,8 +3137,18 @@ function promotePrefixedAuthEnvironment(env) {
@@ -3135,8 +3137,21 @@ function promotePrefixedAuthEnvironment(env) {
}
return protectedKeys;
}
@@ -63,17 +63,20 @@ index d454fd7..991d21d 100644
+ const prototype = Object.getPrototypeOf(value);
+ return (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry) => typeof entry === "string");
+}
+function buildAgentEnvironment(authCredentials, sessionEnv, spawnEnvironment) {
+function resolveAgentEnvironment(spawnEnvironment) {
+ let sourceEnvironment = process.env;
+ if (spawnEnvironment !== void 0) {
+ sourceEnvironment = spawnEnvironment();
+ if (!isPlainStringEnvironment(sourceEnvironment)) throw new TypeError("ACPX spawn environment must be a plain record of string values");
+ }
+ const env = { ...sourceEnvironment };
+ return sourceEnvironment;
+}
+function buildAgentEnvironment(authCredentials, sessionEnv, spawnEnvironment) {
+ const env = { ...resolveAgentEnvironment(spawnEnvironment) };
const protectedAuthEnvKeys = promotePrefixedAuthEnvironment(env);
if (authCredentials) for (const [methodId, credential] of Object.entries(authCredentials)) {
addAuthCredentialEnvKeys(protectedAuthEnvKeys, methodId, credential);
@@ -3178,10 +3190,10 @@ function resolveConfiguredAuthCredential(methodId, authCredentials) {
@@ -3178,10 +3193,10 @@ function resolveConfiguredAuthCredential(methodId, authCredentials) {
const configCredentials = authCredentials ?? {};
return configCredentials[methodId] ?? configCredentials[toEnvToken(methodId)];
}
@@ -86,7 +89,7 @@ index d454fd7..991d21d 100644
stdio: [
"pipe",
"pipe",
@@ -3961,10 +3973,13 @@ function resolveClientCapabilities(params) {
@@ -3961,10 +3976,13 @@ function resolveClientCapabilities(params) {
...params.elicitationModes.includes("url") ? { url: {} } : {}
} } : {}
};
@@ -102,7 +105,7 @@ index d454fd7..991d21d 100644
};
}
function hasResponseField(response, field) {
@@ -4216,9 +4231,21 @@ var AcpClient = class {
@@ -4216,9 +4234,21 @@ var AcpClient = class {
this.lastAgentExit = void 0;
this.lastKnownPid = child.pid ?? void 0;
this.attachAgentLifecycleObservers(child);
@@ -124,7 +127,7 @@ index d454fd7..991d21d 100644
if (!this.options.verbose) return;
process.stderr.write(chunk);
});
@@ -4253,7 +4280,12 @@ var AcpClient = class {
@@ -4253,7 +4283,12 @@ var AcpClient = class {
geminiAcp: isGeminiAcpCommand(spawnCommand, args),
copilotAcp: isCopilotAcpCommand(spawnCommand, args),
claudeAcp: isClaudeAcpCommand(spawnCommand, args),
@@ -138,7 +141,7 @@ index d454fd7..991d21d 100644
};
}
logAgentLaunch(plan) {
@@ -4279,10 +4311,17 @@ var AcpClient = class {
@@ -4279,10 +4314,17 @@ var AcpClient = class {
}
async spawnAgentProcess(plan) {
const spawnCommand = buildAgentSpawnCommand(plan.spawnCommand, plan.args, process.platform, plan.spawnOptions.env);
@@ -158,7 +161,7 @@ index d454fd7..991d21d 100644
try {
await waitForSpawn$1(spawnedChild);
} catch (error) {
@@ -4422,7 +4461,10 @@ var AcpClient = class {
@@ -4422,7 +4464,10 @@ var AcpClient = class {
const createPromise = this.runConnectionRequest(() => connection.newSession({
cwd: sessionCwd,
mcpServers: this.options.mcpServers ?? [],
@@ -170,25 +173,25 @@ index d454fd7..991d21d 100644
}));
result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise;
} catch (error) {
@@ -4864,7 +4906,7 @@ var AcpClient = class {
@@ -4864,7 +4909,7 @@ var AcpClient = class {
}
selectAuthMethod(methods) {
for (const method of methods) {
- const envCredential = readEnvCredential(method.id);
+ const envCredential = readEnvCredential(method.id, this.options.spawnEnvironment?.() ?? process.env);
+ const envCredential = readEnvCredential(method.id, resolveAgentEnvironment(this.options.spawnEnvironment));
if (envCredential) return {
methodId: method.id,
credential: envCredential,
@@ -4890,7 +4932,7 @@ var AcpClient = class {
@@ -4890,7 +4935,7 @@ var AcpClient = class {
}
readAgentSpecificEnvCredential(methodId) {
if (!this.isGrokBuildAcpCommand() || methodId !== "xai.api_key") return;
- const value = process.env.XAI_API_KEY;
+ const value = (this.options.spawnEnvironment?.() ?? process.env).XAI_API_KEY;
+ const value = (resolveAgentEnvironment(this.options.spawnEnvironment)).XAI_API_KEY;
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
}
selectAgentManagedAuthMethod(methodId) {
@@ -5028,6 +5070,12 @@ var AcpClient = class {
@@ -5028,6 +5073,12 @@ var AcpClient = class {
attachAgentLifecycleObservers(child) {
child.once("exit", (exitCode, signal) => {
this.recordAgentExit("process_exit", exitCode, signal);
@@ -201,7 +204,7 @@ index d454fd7..991d21d 100644
});
child.once("close", (exitCode, signal) => {
this.recordAgentExit("process_close", exitCode, signal);
@@ -6435,6 +6483,27 @@ async function withConnectedSession(options) {
@@ -6435,6 +6486,27 @@ async function withConnectedSession(options) {
//#region src/runtime/engine/prompt-turn.ts
const SESSION_REPLY_IDLE_MS = 1e3;
const SESSION_REPLY_DRAIN_TIMEOUT_MS = 5e3;
@@ -229,7 +232,7 @@ index d454fd7..991d21d 100644
async function runPromptTurn(params) {
try {
const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestStarted, params.onElicitation);
@@ -6444,6 +6513,19 @@ async function runPromptTurn(params) {
@@ -6444,6 +6516,19 @@ async function runPromptTurn(params) {
idleMs: SESSION_REPLY_IDLE_MS,
timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS
}).catch(() => {});