Files
PaperClipAI/packages/adapter-utils/src/codex-shell-environment.ts
T
AlexFrontegg 57658fcb30 fix(codex): pass the run's GitHub broker capability to shell commands (#17605)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When an execution has managed GitHub, Paperclip gives it token-free
`git` and `gh` launchers. Each launcher call gets the run's credential
from the broker with a per-run capability
(`PAPERCLIP_GITHUB_BROKER_TOKEN`).
> - The launchers run inside the agent's shell commands and read the
capability from their environment.
> - #17216 limits the environment of Codex shell commands to an
allowlist, and it keeps every `PAPERCLIP_GITHUB_*` name out.
> - So on legacy Codex through ACP, the launchers do not get the
capability, report `capability_missing`, and continue without
credentials.
> - The native runner already passes the same names to its Codex
provider (`GITHUB_CREDENTIAL_ENVIRONMENT_KEYS` in
`packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs`).
> - This pull request adds the broker capability to the legacy Codex
shell allowlist when Paperclip set it for the run.
> - The benefit is that managed `git` and `gh` work again for legacy
Codex agents.

## Linked Issues or Issue Description

Fixes #17599
Refs #16549, #17216

## What Changed

- `packages/adapter-utils/src/codex-shell-environment.ts`: the shell
allowlist also includes `PAPERCLIP_GITHUB_BROKER_TOKEN` and
`PAPERCLIP_GITHUB_BRIDGE_TOKEN` when they have a value in the run's
environment. Only the names enter
`shell_environment_policy.include_only`. The values stay out of the
Codex config and the arguments.
- `packages/adapter-utils/src/codex-shell-environment.test.ts`: a new
test for the capability. The "runtime transports" test no longer lists
the broker token, because the launchers need it in the shell.

## Verification

- `pnpm --filter @paperclipai/adapter-utils exec vitest run
src/codex-shell-environment.test.ts`: 5 passed. The new test fails
without the change.
- Before the change, on a deployment with a legacy Codex agent that has
managed GitHub, `gh pr view` in a shell command fails with `Paperclip:
GitHub capability_missing; continuing without managed credentials`, and
the shell has the broker URL but not the broker capability. I will add
the result after the change is deployed there.

## Risks

- Codex shell commands can now read the run's broker capability. The
broker accepts it only while the run is live (`captureRunIdentity`
requires `status === "running"`), and it returns the credential that the
managed `gh` command receives anyway.
- Agents cannot set these names in their configuration.
`stripForbiddenEnvBindings` removes them
(`server/src/services/heartbeat/run-preparation.ts`) before the server
adds the run's own values.
- No change for runs without managed GitHub: Paperclip does not set the
capability, so the allowlist does not add it.

## Model Used

- Provider and model: Anthropic Claude Opus 5.5 (`claude-opus-5-5`),
used through Claude Code.
- Capabilities used: tool use, code execution (local tests and type
checks), and repository search.
- A human reviewed the diagnosis and the change before this pull request
was opened.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge
2026-10-29 13:18:49 -07:00

35 lines
1.9 KiB
TypeScript

import { AGENT_OBSERVABILITY_ENV_KEYS } from "./agent-observability.js";
const SECRET_NAME = /key|secret|token/i;
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
const PROVIDER_ONLY_KEYS: ReadonlySet<string> = new Set([
"OPENAI_API_KEY", "CODEX_API_KEY", "PAPERCLIP_AI_PROVIDER_KEY",
...AGENT_OBSERVABILITY_ENV_KEYS,
]);
const IDENTITY_KEYS = ["PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"];
// Paperclip's gh and git launchers in the shell fetch the run's GitHub credential with this capability.
const GITHUB_LAUNCHER_KEYS = ["PAPERCLIP_GITHUB_BROKER_TOKEN", "PAPERCLIP_GITHUB_BRIDGE_TOKEN"];
/** Only resolved adapter configuration can opt a service secret into shell access.
* The launch environment here is Paperclip-built, never ambient process.env.
* Return names only: values must stay out of provider config and argv.
*/
export function codexShellEnvironmentKeys(
environment: Record<string, string>,
configuredEnvironment: Record<string, unknown>,
hasAgentIdentity: boolean,
): string[] {
const configuredSecrets = new Set(Object.entries(configuredEnvironment)
.filter(([key, value]) => ENVIRONMENT_NAME.test(key) && typeof value === "string" && typeof environment[key] === "string"
&& !/^PAPERCLIP_(?:GITHUB_|RUNNER_|NATIVE_|ACPX_)/i.test(key)
&& SECRET_NAME.test(key) && !PROVIDER_ONLY_KEYS.has(key.toUpperCase()))
.map(([key]) => key));
return [...new Set([
"PATH", "HOME", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "TEMP", "TMP", "CODEX_HOME",
...Object.keys(environment).filter(key => ENVIRONMENT_NAME.test(key) && !PROVIDER_ONLY_KEYS.has(key.toUpperCase())
&& (key === "PAPERCLIP_API_KEY" || configuredSecrets.has(key) || !SECRET_NAME.test(key))),
...GITHUB_LAUNCHER_KEYS.filter(key => environment[key]),
...(hasAgentIdentity ? IDENTITY_KEYS : []),
])].sort();
}