fix: manage codex home per company by default

This commit is contained in:
dotta
2026-03-20 14:44:27 -05:00
parent 07757a59e9
commit d53714a145
5 changed files with 122 additions and 28 deletions

View File

@@ -41,6 +41,7 @@ Operational fields:
Notes:
- Prompts are piped via stdin (Codex receives "-" prompt argument).
- Paperclip injects desired local skills into the active workspace's ".agents/skills" directory at execution time so Codex can discover "$paperclip" and related skills without coupling them to the user's login home.
- Unless explicitly overridden in adapter config, Paperclip runs Codex with a per-company managed CODEX_HOME under the active Paperclip instance and seeds auth/config from the shared Codex home (the CODEX_HOME env var, when set, or ~/.codex).
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
`;

View File

@@ -6,6 +6,7 @@ import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
const TRUTHY_ENV_RE = /^(1|true|yes|on)$/i;
const COPIED_SHARED_FILES = ["config.json", "config.toml", "instructions.md"] as const;
const SYMLINKED_SHARED_FILES = ["auth.json"] as const;
const DEFAULT_PAPERCLIP_INSTANCE_ID = "default";
function nonEmpty(value: string | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
@@ -15,35 +16,26 @@ export async function pathExists(candidate: string): Promise<boolean> {
return fs.access(candidate).then(() => true).catch(() => false);
}
export function resolveCodexHomeDir(
export function resolveSharedCodexHomeDir(
env: NodeJS.ProcessEnv = process.env,
companyId?: string,
): string {
const fromEnv = nonEmpty(env.CODEX_HOME);
const baseHome = fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), ".codex");
return companyId ? path.join(baseHome, "companies", companyId) : baseHome;
return fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), ".codex");
}
function isWorktreeMode(env: NodeJS.ProcessEnv): boolean {
return TRUTHY_ENV_RE.test(env.PAPERCLIP_IN_WORKTREE ?? "");
}
function resolveWorktreeCodexHomeDir(
export function resolveManagedCodexHomeDir(
env: NodeJS.ProcessEnv,
companyId?: string,
): string | null {
if (!isWorktreeMode(env)) return null;
const paperclipHome = nonEmpty(env.PAPERCLIP_HOME);
if (!paperclipHome) return null;
const instanceId = nonEmpty(env.PAPERCLIP_INSTANCE_ID);
if (instanceId) {
return companyId
? path.resolve(paperclipHome, "instances", instanceId, "companies", companyId, "codex-home")
: path.resolve(paperclipHome, "instances", instanceId, "codex-home");
}
): string {
const paperclipHome = nonEmpty(env.PAPERCLIP_HOME) ?? path.resolve(os.homedir(), ".paperclip");
const instanceId = nonEmpty(env.PAPERCLIP_INSTANCE_ID) ?? DEFAULT_PAPERCLIP_INSTANCE_ID;
return companyId
? path.resolve(paperclipHome, "companies", companyId, "codex-home")
: path.resolve(paperclipHome, "codex-home");
? path.resolve(paperclipHome, "instances", instanceId, "companies", companyId, "codex-home")
: path.resolve(paperclipHome, "instances", instanceId, "codex-home");
}
async function ensureParentDir(target: string): Promise<void> {
@@ -79,15 +71,14 @@ async function ensureCopiedFile(target: string, source: string): Promise<void> {
await fs.copyFile(source, target);
}
export async function prepareWorktreeCodexHome(
export async function prepareManagedCodexHome(
env: NodeJS.ProcessEnv,
onLog: AdapterExecutionContext["onLog"],
companyId?: string,
): Promise<string | null> {
const targetHome = resolveWorktreeCodexHomeDir(env, companyId);
if (!targetHome) return null;
): Promise<string> {
const targetHome = resolveManagedCodexHomeDir(env, companyId);
const sourceHome = resolveCodexHomeDir(env);
const sourceHome = resolveSharedCodexHomeDir(env);
if (path.resolve(sourceHome) === path.resolve(targetHome)) return targetHome;
await fs.mkdir(targetHome, { recursive: true });
@@ -106,7 +97,7 @@ export async function prepareWorktreeCodexHome(
await onLog(
"stdout",
`[paperclip] Using worktree-isolated Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
`[paperclip] Using ${isWorktreeMode(env) ? "worktree-isolated" : "Paperclip-managed"} Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
);
return targetHome;
}

View File

@@ -21,7 +21,7 @@ import {
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
import { pathExists, prepareWorktreeCodexHome, resolveCodexHomeDir } from "./codex-home.js";
import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js";
import { resolveCodexDesiredSkillNames } from "./skills.js";
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -268,10 +268,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
const preparedWorktreeCodexHome =
configuredCodexHome ? null : await prepareWorktreeCodexHome(process.env, onLog, agent.companyId);
const defaultCodexHome = resolveCodexHomeDir(process.env, agent.companyId);
const effectiveCodexHome = configuredCodexHome ?? preparedWorktreeCodexHome ?? defaultCodexHome;
const preparedManagedCodexHome =
configuredCodexHome ? null : await prepareManagedCodexHome(process.env, onLog, agent.companyId);
const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId);
const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome;
await fs.mkdir(effectiveCodexHome, { recursive: true });
const codexWorkspaceSkillsDir = resolveCodexWorkspaceSkillsDir(cwd);
await ensureCodexSkillsInjected(