fix: preserve Codex shell exclusions for configured secrets

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 13:10:25 -05:00
1 parent cf830d631b
commit ea3a6d7e6f
7 files changed
+38 -9

No files matched your search

+2
View File
@@ -96,3 +96,5 @@ Do not use a disaster-recovery backup as a development clone when agents should
have distinct identities.
Native failure records and reports redact the assigned key before truncating diagnostics. Streaming output buffers settle at item or turn completion: short structural prefixes (such as a trailing dash) are preserved, longer interrupted key fragments become redaction markers, and pending buffers are cleared. Terminal events carry these settled `outputTails`; transcript projection displays them as deltas without changing the source event receipt.
Codex shell delivery preserves its default `KEY`/`SECRET`/`TOKEN` name exclusions for other configured credentials. The exceptions are the three identity variables and `PAPERCLIP_API_KEY`: the latter is the short-lived, scoped run/bridge credential required by the Paperclip agent skill’s Bash/curl API calls. Disabling Codex’s automatic exclusions is paired with this explicit filtered allowlist; it does not admit arbitrary host environment variables. Provider authentication secrets can still reach the provider process without being newly exposed to shell commands by this feature.
@@ -550,15 +550,16 @@ describe("shared ACPX engine runtime behavior", () => {
);
});
it("keeps assigned API and tool credentials in the identity-enabled Codex shell environment", async () => {
it("keeps identity and scoped API access without exposing configured service tokens to Codex shells", async () => {
const { meta } = await runExecutor({ agent: "codex", env: { MY_SERVICE_TOKEN: "assigned-tool-token" } }, {
authToken: "assigned-run-token",
agentIdentity: { keyId: "sha256:test", publicKeyPem: "public", privateKeyPem: "private" },
});
const config = JSON.parse(String((meta[0]?.env as Record<string, string>).CODEX_CONFIG));
expect(config.shell_environment_policy.include_only).toEqual(expect.arrayContaining([
"PAPERCLIP_API_KEY", "MY_SERVICE_TOKEN", "PAPERCLIP_AGENT_PRIVATE_KEY",
"PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
]));
expect(config.shell_environment_policy.include_only).not.toContain("MY_SERVICE_TOKEN");
expect(JSON.stringify(config)).not.toContain("assigned-run-token");
expect(JSON.stringify(config)).not.toContain("assigned-tool-token");
});
@@ -2051,7 +2051,9 @@ async function buildRuntime(input: {
...(input.ctx.agentIdentity ? { identityEnvironmentKeys: [...new Set([
"PATH", "HOME", "LANG", "TMPDIR", "CODEX_HOME",
"PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
...Object.keys(env),
// Preserve Codex's default secret-name exclusions. The short-lived
// Paperclip API token is required by the agent skill's Bash/curl calls.
...Object.keys(env).filter(key => key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)),
])] } : {}),
requestedModel,
requestedThinkingEffort,
@@ -190,15 +190,16 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
};
}
it("keeps assigned API and tool credentials in the identity-enabled CLI shell", async () => {
it("keeps identity and scoped API access without exposing configured service tokens to CLI shells", async () => {
vi.stubEnv("DATABASE_URL", "postgres://operator:host-password@host/private");
vi.stubEnv("HOST_DATABASE_PASSWORD", "host-password");
const { commandArgs } = await runTeardown({ sandboxAuth: "{}", hostAuth: "{}", withIdentity: true });
const policy = commandArgs.find(arg => arg.startsWith("shell_environment_policy.include_only="));
const keys = JSON.parse(policy!.slice(policy!.indexOf("=") + 1));
expect(keys).toEqual(expect.arrayContaining([
"PAPERCLIP_API_KEY", "MY_SERVICE_TOKEN", "PAPERCLIP_AGENT_PRIVATE_KEY",
"PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
]));
expect(keys).not.toContain("MY_SERVICE_TOKEN");
expect(keys).not.toContain("DATABASE_URL");
expect(keys).not.toContain("HOST_DATABASE_PASSWORD");
expect(commandArgs.join(" ")).not.toContain("assigned-run-token");
@@ -1232,9 +1232,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const identityNames = ["PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"];
const shellKeys = [...new Set([
"PATH", "HOME", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "TEMP", "TMP", "CODEX_HOME",
// Only explicitly assigned run/config credentials cross this boundary.
// Arbitrary host variables (DATABASE_URL, passwords, etc.) do not.
...Object.keys(env), ...identityNames,
// Keep Codex's default secret-name exclusions except for the scoped
// Paperclip API token used by the agent skill. Never include host env.
...Object.keys(env).filter(key => key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)),
...identityNames,
])];
args.unshift("-c", "features.shell_snapshot=false", "-c", 'shell_environment_policy.inherit="all"', "-c", "shell_environment_policy.ignore_default_excludes=true",
"-c", `shell_environment_policy.include_only=${JSON.stringify(shellKeys)}`);
@@ -52,6 +52,26 @@ describe("ACPX runtime sandbox", () => {
expect((await stat(requirementsPath)).mode & 0o777).toBe(0o600);
});
it("keeps provider credentials outside identity-enabled Codex shell commands", async () => {
const fixture = await sandboxFixture("codex");
const sandbox = await prepareAcpxRuntimeSandbox({
binding: fixture.binding, agent: "codex", environment: {
PATH: process.env.PATH,
OPENAI_API_KEY: "provider-secret", MY_SERVICE_TOKEN: "configured-secret",
PAPERCLIP_API_KEY: "scoped-run-token",
PAPERCLIP_AGENT_KEY_ID: "sha256:test", PAPERCLIP_AGENT_PUBLIC_KEY: "public-identity",
PAPERCLIP_AGENT_PRIVATE_KEY: "private-identity",
},
});
const config = await readFile(join(sandbox.agentHomeDirectory, "config.toml"), "utf8");
const keys = JSON.parse(config.split("\n").find(line => line.startsWith("include_only = "))!.slice("include_only = ".length));
expect(keys).toEqual(expect.arrayContaining(["PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"]));
expect(keys).not.toContain("OPENAI_API_KEY");
expect(keys).not.toContain("MY_SERVICE_TOKEN");
expect(sandbox.launchEnvironment.OPENAI_API_KEY).toBe("provider-secret");
for (const value of ["provider-secret", "configured-secret", "scoped-run-token", "private-identity"]) expect(config).not.toContain(value);
});
it.each(["claude-sonnet-5", "sonnet", "custom-deployment-id"])(
"preserves the requested Claude model %s in its isolated settings",
async (model) => {
@@ -458,7 +458,9 @@ export async function prepareAcpxRuntimeSandbox(input: {
`include_only = ${JSON.stringify([...new Set([
"PATH", "HOME", "LANG", "LANGUAGE", "TZ", "TMPDIR", "TEMP", "TMP", "CODEX_HOME",
"PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
...Object.keys(input.environment),
// Provider/config secrets keep Codex's default shell exclusions;
// only Paperclip's scoped API token is required by Bash/curl skills.
...Object.keys(input.environment).filter(key => key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)),
])])}`,
] : []),
"",