mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
fix: preserve Codex shell exclusions for configured secrets
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
cf830d631b
commit
ea3a6d7e6f
7 files changed
+38
-9
No files matched your search
@@ -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)),
|
||||
])])}`,
|
||||
] : []),
|
||||
"",
|
||||
|
||||
Reference in new issue
Block a user