fix(runner): isolate local smoke credentials per profile

This commit is contained in:
Dotta 2026-09-03 17:57:56 -05:00
parent 7fe94196ba
commit 24e0b9776b
4 changed files with 81 additions and 19 deletions

View File

@ -73,7 +73,7 @@
"typecheck:rust": "cargo fmt --manifest-path runner/Cargo.toml --all -- --check && cargo check --manifest-path runner/Cargo.toml --locked --workspace",
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/materialize-opencode-binary.test.mjs && vitest run",
"test:typescript": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs && vitest run",
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
"test:durable": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core durable::",

View File

@ -0,0 +1,15 @@
export async function withIsolatedProfileCredentials(input) {
for (const name of input.providerCredentialNames) {
delete input.environment[name];
}
for (const [name, value] of Object.entries(input.profileCredentials)) {
input.environment[name] = value;
}
try {
return await input.run();
} finally {
for (const name of input.providerCredentialNames) {
delete input.environment[name];
}
}
}

View File

@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import test from "node:test";
import { withIsolatedProfileCredentials } from "./local-provider-smoke-environment.mjs";
test("a later smoke profile cannot observe another provider credential", async () => {
const environment = {
PATH: "/bin",
OPENAI_API_KEY: "credential-from-an-earlier-profile",
};
const providerCredentialNames = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"];
await assert.rejects(
withIsolatedProfileCredentials({
environment,
providerCredentialNames,
profileCredentials: {
ANTHROPIC_API_KEY: "credential-for-current-profile",
},
run: async () => {
assert.equal(environment.OPENAI_API_KEY, undefined);
assert.equal(
environment.ANTHROPIC_API_KEY,
"credential-for-current-profile",
);
assert.equal(environment.PATH, "/bin");
throw new Error("provider failed");
},
}),
/provider failed/,
);
assert.deepEqual(environment, { PATH: "/bin" });
});

View File

@ -4,6 +4,8 @@ import { access, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import { withIsolatedProfileCredentials } from "./local-provider-smoke-environment.mjs";
const PROFILE_IDS = [
"runner-acpx-claude",
"runner-acpx-codex",
@ -244,12 +246,14 @@ try {
liveCandidate(profile, RUNNER_LIVE_CANDIDATE_SLOTS),
]),
);
const credentialsByProfile = new Map();
for (const [profile, candidate] of candidates) {
if (!candidate) throw new Error(`Missing live candidate for ${profile}`);
const missingCredentials = [];
const profileCredentials = {};
for (const name of candidate.qualification.requiredEnvironment) {
const value = ambientEnvironment[name]?.trim();
if (value) process.env[name] = value;
if (value) profileCredentials[name] = value;
else missingCredentials.push(name);
}
if (missingCredentials.length > 0) {
@ -257,13 +261,16 @@ try {
`${profile} requires ${missingCredentials.join(", ")} in the smoke process environment`,
);
}
credentialsByProfile.set(profile, profileCredentials);
}
const providerCredentialNames = new Set(
[...credentialsByProfile.values()].flatMap((credentials) =>
Object.keys(credentials),
),
);
const credentialValues = new Set(
[...candidates.values()].flatMap((candidate) =>
candidate.qualification.requiredEnvironment.flatMap((name) => {
const value = ambientEnvironment[name]?.trim();
return value ? [value] : [];
}),
[...credentialsByProfile.values()].flatMap((credentials) =>
Object.values(credentials),
),
);
const evalCase = runnerWorkflowCase("completion-robustness");
@ -283,18 +290,24 @@ try {
budget: candidate.budget,
};
try {
const observation = await executeLiveRunnerWorkflow({
entry,
candidate,
evalCase,
workingDirectory: workspace,
// This smoke proves the provider launch/message/semantic-terminal path.
// Usage conformance remains covered by the dedicated eval campaign.
allowMissingUsage: true,
expectedAssistantText: "PAPERCLIP_LOCAL_PROVIDER_SMOKE_OK",
promptOverride:
"Reply with exactly PAPERCLIP_LOCAL_PROVIDER_SMOKE_OK and no other text. Do not call tools.",
runnerBinary: runnerd,
const observation = await withIsolatedProfileCredentials({
environment: process.env,
providerCredentialNames,
profileCredentials: credentialsByProfile.get(profile),
run: () =>
executeLiveRunnerWorkflow({
entry,
candidate,
evalCase,
workingDirectory: workspace,
// This smoke proves the provider launch/message/semantic-terminal path.
// Usage conformance remains covered by the dedicated eval campaign.
allowMissingUsage: true,
expectedAssistantText: "PAPERCLIP_LOCAL_PROVIDER_SMOKE_OK",
promptOverride:
"Reply with exactly PAPERCLIP_LOCAL_PROVIDER_SMOKE_OK and no other text. Do not call tools.",
runnerBinary: runnerd,
}),
});
const failures = failedChecks(observation);
const passed = failures.length === 0;