Dev #11
@@ -71,18 +71,46 @@ async function ensureCopiedFile(target: string, source: string): Promise<void> {
|
|||||||
await fs.copyFile(source, target);
|
await fs.copyFile(source, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an `auth.json` containing only `OPENAI_API_KEY` so the codex CLI can
|
||||||
|
* authenticate via API key. Overwrites any existing file or symlink at that
|
||||||
|
* path. Required because the codex CLI (>= 0.122) ignores the `OPENAI_API_KEY`
|
||||||
|
* environment variable and only reads credentials from `$CODEX_HOME/auth.json`.
|
||||||
|
*/
|
||||||
|
export async function writeApiKeyAuthJson(home: string, apiKey: string): Promise<void> {
|
||||||
|
await fs.mkdir(home, { recursive: true });
|
||||||
|
const target = path.join(home, "auth.json");
|
||||||
|
await fs.rm(target, { force: true });
|
||||||
|
await fs.writeFile(target, JSON.stringify({ OPENAI_API_KEY: apiKey }), { mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
export async function prepareManagedCodexHome(
|
export async function prepareManagedCodexHome(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
onLog: AdapterExecutionContext["onLog"],
|
onLog: AdapterExecutionContext["onLog"],
|
||||||
companyId?: string,
|
companyId?: string,
|
||||||
|
options: { apiKey?: string | null } = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const targetHome = resolveManagedCodexHomeDir(env, companyId);
|
const targetHome = resolveManagedCodexHomeDir(env, companyId);
|
||||||
|
const apiKey = nonEmpty(options.apiKey ?? undefined);
|
||||||
|
|
||||||
const sourceHome = resolveSharedCodexHomeDir(env);
|
const sourceHome = resolveSharedCodexHomeDir(env);
|
||||||
if (path.resolve(sourceHome) === path.resolve(targetHome)) return targetHome;
|
const seedFromShared = path.resolve(sourceHome) !== path.resolve(targetHome);
|
||||||
|
|
||||||
await fs.mkdir(targetHome, { recursive: true });
|
await fs.mkdir(targetHome, { recursive: true });
|
||||||
|
|
||||||
|
// If a previous run wrote an apikey-mode auth.json (regular file) and this
|
||||||
|
// run has no apiKey, remove it so the chatgpt-mode symlink can be restored.
|
||||||
|
// Without this cleanup, ensureSymlink bails on a non-symlink and Codex keeps
|
||||||
|
// authenticating with the stale key after it is removed from configuration.
|
||||||
|
if (!apiKey && seedFromShared) {
|
||||||
|
const authPath = path.join(targetHome, "auth.json");
|
||||||
|
const existing = await fs.lstat(authPath).catch(() => null);
|
||||||
|
if (existing && !existing.isSymbolicLink()) {
|
||||||
|
await fs.rm(authPath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seedFromShared) {
|
||||||
for (const name of SYMLINKED_SHARED_FILES) {
|
for (const name of SYMLINKED_SHARED_FILES) {
|
||||||
const source = path.join(sourceHome, name);
|
const source = path.join(sourceHome, name);
|
||||||
if (!(await pathExists(source))) continue;
|
if (!(await pathExists(source))) continue;
|
||||||
@@ -99,5 +127,15 @@ export async function prepareManagedCodexHome(
|
|||||||
"stdout",
|
"stdout",
|
||||||
`[paperclip] Using ${isWorktreeMode(env) ? "worktree-isolated" : "Paperclip-managed"} Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
|
`[paperclip] Using ${isWorktreeMode(env) ? "worktree-isolated" : "Paperclip-managed"} Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
await writeApiKeyAuthJson(targetHome, apiKey);
|
||||||
|
await onLog(
|
||||||
|
"stdout",
|
||||||
|
`[paperclip] Wrote API-key auth.json into Codex home "${targetHome}" from configured OPENAI_API_KEY.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return targetHome;
|
return targetHome;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,8 +332,16 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
||||||
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
||||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
|
const configuredOpenAiApiKey =
|
||||||
|
typeof envConfig.OPENAI_API_KEY === "string" && envConfig.OPENAI_API_KEY.trim().length > 0
|
||||||
|
? envConfig.OPENAI_API_KEY.trim()
|
||||||
|
: null;
|
||||||
const preparedManagedCodexHome =
|
const preparedManagedCodexHome =
|
||||||
configuredCodexHome ? null : await prepareManagedCodexHome(process.env, onLog, agent.companyId);
|
configuredCodexHome
|
||||||
|
? null
|
||||||
|
: await prepareManagedCodexHome(process.env, onLog, agent.companyId, {
|
||||||
|
apiKey: configuredOpenAiApiKey,
|
||||||
|
});
|
||||||
const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId);
|
const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId);
|
||||||
const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome;
|
const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome;
|
||||||
await fs.mkdir(effectiveCodexHome, { recursive: true });
|
await fs.mkdir(effectiveCodexHome, { recursive: true });
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
resolveAdapterExecutionTargetCwd,
|
resolveAdapterExecutionTargetCwd,
|
||||||
} from "@paperclipai/adapter-utils/execution-target";
|
} from "@paperclipai/adapter-utils/execution-target";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
import { parseCodexJsonl } from "./parse.js";
|
import { parseCodexJsonl } from "./parse.js";
|
||||||
import { codexHomeDir, readCodexAuthInfo } from "./quota.js";
|
import { codexHomeDir, readCodexAuthInfo } from "./quota.js";
|
||||||
import { buildCodexExecArgs } from "./codex-args.js";
|
import { buildCodexExecArgs } from "./codex-args.js";
|
||||||
@@ -174,14 +175,45 @@ export async function testEnvironment(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codex CLI (>= 0.122) ignores the OPENAI_API_KEY env var and only reads
|
||||||
|
// credentials from $CODEX_HOME/auth.json. When we have a key available,
|
||||||
|
// wrap the probe with a shell that materializes a per-run auth.json so
|
||||||
|
// the CLI can authenticate. The key content is passed via env (not on
|
||||||
|
// the command line) to avoid leaking it into process listings.
|
||||||
|
const probeApiKey = isNonEmpty(configOpenAiKey)
|
||||||
|
? configOpenAiKey
|
||||||
|
: isNonEmpty(hostOpenAiKey)
|
||||||
|
? hostOpenAiKey
|
||||||
|
: null;
|
||||||
|
let probeCommand = command;
|
||||||
|
let probeArgs = args;
|
||||||
|
const probeEnv: Record<string, string> = { ...env };
|
||||||
|
if (probeApiKey) {
|
||||||
|
const probeHome = targetIsRemote
|
||||||
|
? `/tmp/paperclip-codex-probe-${runId}`
|
||||||
|
: path.join(os.tmpdir(), `paperclip-codex-probe-${runId}`);
|
||||||
|
probeEnv.CODEX_HOME = probeHome;
|
||||||
|
probeEnv._PAPERCLIP_CODEX_AUTH_JSON = JSON.stringify({ OPENAI_API_KEY: probeApiKey });
|
||||||
|
probeCommand = "sh";
|
||||||
|
// Trap on EXIT removes the probe home (with the API-key auth.json) on
|
||||||
|
// any exit path; we drop `exec` so the wrapper shell stays alive long
|
||||||
|
// enough for the trap to fire after the child returns.
|
||||||
|
probeArgs = [
|
||||||
|
"-c",
|
||||||
|
'set -e; mkdir -p "$CODEX_HOME"; umask 077; printf "%s" "$_PAPERCLIP_CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"; unset _PAPERCLIP_CODEX_AUTH_JSON; trap \'rm -rf "$CODEX_HOME"\' EXIT INT TERM; "$0" "$@"',
|
||||||
|
command,
|
||||||
|
...args,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const probe = await runAdapterExecutionTargetProcess(
|
const probe = await runAdapterExecutionTargetProcess(
|
||||||
runId,
|
runId,
|
||||||
target,
|
target,
|
||||||
command,
|
probeCommand,
|
||||||
args,
|
probeArgs,
|
||||||
{
|
{
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
env: probeEnv,
|
||||||
timeoutSec: 45,
|
timeoutSec: 45,
|
||||||
graceSec: 5,
|
graceSec: 5,
|
||||||
stdin: "Respond with hello.",
|
stdin: "Respond with hello.",
|
||||||
@@ -221,7 +253,9 @@ export async function testEnvironment(
|
|||||||
level: "warn",
|
level: "warn",
|
||||||
message: "Codex CLI is installed, but authentication is not ready.",
|
message: "Codex CLI is installed, but authentication is not ready.",
|
||||||
...(detail ? { detail } : {}),
|
...(detail ? { detail } : {}),
|
||||||
hint: "Configure OPENAI_API_KEY in adapter env/shell or run `codex login`, then retry the probe.",
|
hint: probeApiKey
|
||||||
|
? "OPENAI_API_KEY was provided but Codex still rejected the request. Verify the key is valid for the OpenAI Responses API (e.g. `curl -H \"Authorization: Bearer $OPENAI_API_KEY\" https://api.openai.com/v1/models`), or run `codex login` and seed `~/.codex/auth.json`."
|
||||||
|
: "Codex CLI does not read OPENAI_API_KEY from the environment; set OPENAI_API_KEY in this adapter's config (so Paperclip writes it to `$CODEX_HOME/auth.json`) or run `codex login` on the host first.",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
checks.push({
|
checks.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user