fix(codex): pass MCP bearer tokens through env

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Patrick Ferdig 2026-09-12 11:28:30 -06:00
parent 0e14c61da7
commit 495a8fc95e
4 changed files with 24 additions and 5 deletions

View File

@ -1023,7 +1023,8 @@ describe("evaluateCodexCredentialReadiness", () => {
const alpha = await fs.readFile(path.join(alphaHome, "config.toml"), "utf8");
const zero = await fs.readFile(path.join(zeroHome, "config.toml"), "utf8");
expect(alpha).toContain('[mcp_servers."alpha"]');
expect(alpha).toContain('Authorization = "Bearer alpha-token"');
expect(alpha).toContain('bearer_token_env_var = "PAPERCLIP_MANAGED_MCP_BEARER_TOKEN_1"');
expect(alpha).not.toContain("alpha-token");
expect(zero).not.toContain("mcp_servers.");
expect(zero).not.toContain("stale-token");
expect(alphaHome).not.toBe(zeroHome);
@ -1142,7 +1143,7 @@ describe("stageCodexHomeForSync", () => {
}
});
// config.toml carries the managed MCP `Authorization: Bearer …` header and is
// config.toml carries only the managed MCP bearer-token environment variable name and is
// secret-bearing; the staged copy must be 0600, not the world-readable default.
it("writes the staged config.toml (managed MCP bearer header) with mode 0600", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-toml-mode-"));
@ -1154,7 +1155,7 @@ describe("stageCodexHomeForSync", () => {
// and is persisted 0600 on disk.
await fs.writeFile(
path.join(home, "config.toml"),
"[mcp_servers.paperclip]\nheaders = { Authorization = \"Bearer secret-token\" }\n",
"[mcp_servers.paperclip]\nbearer_token_env_var = \"PAPERCLIP_MANAGED_MCP_BEARER_TOKEN_1\"\n",
{ mode: 0o600 },
);
staged = await stageCodexHomeForSync(home, { runId: "run-toml-mode" });

View File

@ -10,6 +10,7 @@ const COPIED_SHARED_FILES = ["config.json", "config.toml", "instructions.md"] as
const SYMLINKED_SHARED_FILES = ["auth.json"] as const;
const MANAGED_MCP_BLOCK_START = "# BEGIN PAPERCLIP MANAGED MCP";
const MANAGED_MCP_BLOCK_END = "# END PAPERCLIP MANAGED MCP";
const MANAGED_MCP_BEARER_TOKEN_ENV_PREFIX = "PAPERCLIP_MANAGED_MCP_BEARER_TOKEN_";
/**
* The allowlist of managed `CODEX_HOME` entries that the codex-local adapter
@ -33,6 +34,17 @@ export type ManagedCodexMcpGateway = {
bearerToken: string;
};
export function managedCodexMcpBearerTokenEnv(
gateways: ManagedCodexMcpGateway[],
): Record<string, string> {
return Object.fromEntries(
gateways.map((gateway, index) => [
`${MANAGED_MCP_BEARER_TOKEN_ENV_PREFIX}${index + 1}`,
gateway.bearerToken,
]),
);
}
export function mergeManagedCodexMcpGateways(
primary: ManagedCodexMcpGateway[],
secondary: ManagedCodexMcpGateway[],
@ -316,7 +328,7 @@ function buildManagedMcpBlock(input: {
"",
`[mcp_servers.${tomlString(managedName)}]`,
`url = ${tomlString(url)}`,
`headers = { Authorization = ${tomlString(`Bearer ${gateway.bearerToken}`)} }`,
`bearer_token_env_var = ${tomlString(`${MANAGED_MCP_BEARER_TOKEN_ENV_PREFIX}${index + 1}`)}`,
);
});
lines.push(MANAGED_MCP_BLOCK_END);

View File

@ -78,6 +78,7 @@ import {
resolveSharedCodexHomeDir,
seedManagedCodexHome,
stageCodexHomeForSync,
managedCodexMcpBearerTokenEnv,
mergeManagedCodexMcpGateways,
writeManagedCodexMcpConfig,
type ManagedCodexMcpGateway,
@ -891,6 +892,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
onEvent,
});
const env: Record<string, string> = { ...paperclipBaseEnv };
Object.assign(env, managedCodexMcpBearerTokenEnv(managedMcpGateways));
env.PAPERCLIP_RUN_ID = runId;
const wakeTaskId =
(typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||

View File

@ -23,6 +23,7 @@ const payload = {
paperclipApiUrl: process.env.PAPERCLIP_API_URL || null,
paperclipApiKey: process.env.PAPERCLIP_API_KEY || null,
paperclipApiBridgeMode: process.env.PAPERCLIP_API_BRIDGE_MODE || null,
managedMcpBearerToken: process.env.PAPERCLIP_MANAGED_MCP_BEARER_TOKEN_1 || null,
paperclipEnvKeys: Object.keys(process.env)
.filter((key) => key.startsWith("PAPERCLIP_"))
.sort(),
@ -56,6 +57,7 @@ type CapturePayload = {
paperclipApiUrl?: string | null;
paperclipApiKey?: string | null;
paperclipApiBridgeMode?: string | null;
managedMcpBearerToken?: string | null;
paperclipEnvKeys: string[];
};
@ -313,7 +315,9 @@ describe("codex execute", () => {
expect(configText).toContain("[mcp_servers.github]");
expect(configText).toContain("[mcp_servers.\"paperclip-github\"]");
expect(configText).toContain('url = "http://paperclip.local:3100/api/tool-gateway/gateways/gateway-1/mcp"');
expect(configText).toContain('Authorization = "Bearer pcgw_secret-managed-token"');
expect(configText).toContain('bearer_token_env_var = "PAPERCLIP_MANAGED_MCP_BEARER_TOKEN_1"');
expect(configText).not.toContain("pcgw_secret-managed-token");
expect(capture.managedMcpBearerToken).toBe("pcgw_secret-managed-token");
expect(logs).toEqual(
expect.arrayContaining([
expect.objectContaining({