mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(acpx): reject invalid credential environment callbacks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
9a0ac12318
commit
d5905a5489
3 files changed
+42
-17
No files matched your search
@@ -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
@@ -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(() => {});
|
||||
|
||||
Reference in new issue
Block a user