Merge pull request #1782 from paperclipai/fix/codex-skill-injection-location
fix(codex): inject skills into ~/.codex/skills/ instead of workspace
This commit is contained in:
commit
6ebfc0ff3d
5 changed files with 14 additions and 12 deletions
|
|
@ -42,7 +42,7 @@ Notes:
|
||||||
- Prompts are piped via stdin (Codex receives "-" prompt argument).
|
- Prompts are piped via stdin (Codex receives "-" prompt argument).
|
||||||
- If instructionsFilePath is configured, Paperclip prepends that file's contents to the stdin prompt on every run.
|
- If instructionsFilePath is configured, Paperclip prepends that file's contents to the stdin prompt on every run.
|
||||||
- Codex exec automatically applies repo-scoped AGENTS.md instructions from the active workspace. Paperclip cannot suppress that discovery in exec mode, so repo AGENTS.md files may still apply even when you only configured an explicit instructionsFilePath.
|
- Codex exec automatically applies repo-scoped AGENTS.md instructions from the active workspace. Paperclip cannot suppress that discovery in exec mode, so repo AGENTS.md files may still apply even when you only configured an explicit instructionsFilePath.
|
||||||
- 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.
|
- Paperclip injects desired local skills into the effective CODEX_HOME/skills/ directory at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. In managed-home mode (the default) this is ~/.paperclip/instances/<id>/companies/<companyId>/codex-home/skills/; when CODEX_HOME is explicitly overridden in adapter config, that override is used instead.
|
||||||
- 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).
|
- 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).
|
- 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.
|
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
||||||
import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js";
|
import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js";
|
||||||
import { resolveCodexDesiredSkillNames } from "./skills.js";
|
import { resolveCodexDesiredSkillNames } from "./skills.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -135,8 +135,8 @@ async function pruneBrokenUnavailablePaperclipSkillSymlinks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCodexWorkspaceSkillsDir(cwd: string): string {
|
function resolveCodexSkillsDir(codexHome: string): string {
|
||||||
return path.join(cwd, ".agents", "skills");
|
return path.join(codexHome, "skills");
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnsureCodexSkillsInjectedOptions = {
|
type EnsureCodexSkillsInjectedOptions = {
|
||||||
|
|
@ -157,7 +157,7 @@ export async function ensureCodexSkillsInjected(
|
||||||
const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key));
|
const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key));
|
||||||
if (skillsEntries.length === 0) return;
|
if (skillsEntries.length === 0) return;
|
||||||
|
|
||||||
const skillsHome = options.skillsHome ?? resolveCodexWorkspaceSkillsDir(process.cwd());
|
const skillsHome = options.skillsHome ?? resolveCodexSkillsDir(resolveSharedCodexHomeDir());
|
||||||
await fs.mkdir(skillsHome, { recursive: true });
|
await fs.mkdir(skillsHome, { recursive: true });
|
||||||
const linkSkill = options.linkSkill;
|
const linkSkill = options.linkSkill;
|
||||||
for (const entry of skillsEntries) {
|
for (const entry of skillsEntries) {
|
||||||
|
|
@ -273,11 +273,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
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 });
|
||||||
const codexWorkspaceSkillsDir = resolveCodexWorkspaceSkillsDir(cwd);
|
// Inject skills into the same CODEX_HOME that Codex will actually run with
|
||||||
|
// (managed home in the default case, or an explicit override from adapter config).
|
||||||
|
const codexSkillsDir = resolveCodexSkillsDir(effectiveCodexHome);
|
||||||
await ensureCodexSkillsInjected(
|
await ensureCodexSkillsInjected(
|
||||||
onLog,
|
onLog,
|
||||||
{
|
{
|
||||||
skillsHome: codexWorkspaceSkillsDir,
|
skillsHome: codexSkillsDir,
|
||||||
skillsEntries: codexSkillEntries,
|
skillsEntries: codexSkillEntries,
|
||||||
desiredSkillNames,
|
desiredSkillNames,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ async function buildCodexSkillSnapshot(
|
||||||
sourcePath: entry.source,
|
sourcePath: entry.source,
|
||||||
targetPath: null,
|
targetPath: null,
|
||||||
detail: desiredSet.has(entry.key)
|
detail: desiredSet.has(entry.key)
|
||||||
? "Will be linked into the workspace .agents/skills directory on the next run."
|
? "Will be linked into the effective CODEX_HOME/skills/ directory on the next run."
|
||||||
: null,
|
: null,
|
||||||
required: Boolean(entry.required),
|
required: Boolean(entry.required),
|
||||||
requiredReason: entry.requiredReason ?? null,
|
requiredReason: entry.requiredReason ?? null,
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ describe("codex execute", () => {
|
||||||
"company-1",
|
"company-1",
|
||||||
"codex-home",
|
"codex-home",
|
||||||
);
|
);
|
||||||
const workspaceSkill = path.join(workspace, ".agents", "skills", "paperclip");
|
const homeSkill = path.join(isolatedCodexHome, "skills", "paperclip");
|
||||||
await fs.mkdir(workspace, { recursive: true });
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
await fs.mkdir(sharedCodexHome, { recursive: true });
|
await fs.mkdir(sharedCodexHome, { recursive: true });
|
||||||
await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"token":"shared"}\n', "utf8");
|
await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"token":"shared"}\n', "utf8");
|
||||||
|
|
@ -284,7 +284,7 @@ describe("codex execute", () => {
|
||||||
expect(await fs.realpath(isolatedAuth)).toBe(await fs.realpath(path.join(sharedCodexHome, "auth.json")));
|
expect(await fs.realpath(isolatedAuth)).toBe(await fs.realpath(path.join(sharedCodexHome, "auth.json")));
|
||||||
expect((await fs.lstat(isolatedConfig)).isFile()).toBe(true);
|
expect((await fs.lstat(isolatedConfig)).isFile()).toBe(true);
|
||||||
expect(await fs.readFile(isolatedConfig, "utf8")).toBe('model = "codex-mini-latest"\n');
|
expect(await fs.readFile(isolatedConfig, "utf8")).toBe('model = "codex-mini-latest"\n');
|
||||||
expect((await fs.lstat(workspaceSkill)).isSymbolicLink()).toBe(true);
|
expect((await fs.lstat(homeSkill)).isSymbolicLink()).toBe(true);
|
||||||
expect(logs).toContainEqual(
|
expect(logs).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
stream: "stdout",
|
stream: "stdout",
|
||||||
|
|
@ -371,7 +371,7 @@ describe("codex execute", () => {
|
||||||
|
|
||||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||||
expect(capture.codexHome).toBe(explicitCodexHome);
|
expect(capture.codexHome).toBe(explicitCodexHome);
|
||||||
expect((await fs.lstat(path.join(workspace, ".agents", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
expect((await fs.lstat(path.join(explicitCodexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||||
await expect(fs.lstat(path.join(paperclipHome, "instances", "worktree-1", "codex-home"))).rejects.toThrow();
|
await expect(fs.lstat(path.join(paperclipHome, "instances", "worktree-1", "codex-home"))).rejects.toThrow();
|
||||||
} finally {
|
} finally {
|
||||||
if (previousHome === undefined) delete process.env.HOME;
|
if (previousHome === undefined) delete process.env.HOME;
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ describe("codex local skill sync", () => {
|
||||||
expect(before.desiredSkills).toContain(paperclipKey);
|
expect(before.desiredSkills).toContain(paperclipKey);
|
||||||
expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true);
|
expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true);
|
||||||
expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
|
expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
|
||||||
expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain(".agents/skills");
|
expect(before.entries.find((entry) => entry.key === paperclipKey)?.detail).toContain("CODEX_HOME/skills/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not persist Paperclip skills into CODEX_HOME during sync", async () => {
|
it("does not persist Paperclip skills into CODEX_HOME during sync", async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue