Files
paperclip/server/src/__tests__/codex-local-skill-sync.test.ts
T
Dotta 641eb44949 [codex] Harden create-agent skill governance (#4422)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Hiring agents is a governance-sensitive workflow because it grants
roles, adapter config, skills, and execution capability
> - The create-agent skill needs explicit templates and review guidance
so hires are auditable and not over-permissioned
> - Skill sync also needs to recognize bundled Paperclip skills
consistently for Codex local agents
> - This pull request expands create-agent role templates, adds a
security-engineer template, and documents capability/secret-handling
review requirements
> - The benefit is safer, more repeatable agent creation with clearer
approval payloads and less permission sprawl

## What Changed

- Expanded `paperclip-create-agent` guidance for template selection,
adjacent-template drafting, and role-specific review bars.
- Added a Security Engineer agent template and collaboration/safety
sections for Coder, QA, and UX Designer templates.
- Hardened draft-review guidance around desired skills, external-system
access, secrets, and confidential advisory handling.
- Updated LLM agent-configuration guidance to point hiring workflows at
the create-agent skill.
- Added tests for bundled skill sync, create-agent skill injection, hire
approval payloads, and LLM route guidance.

## Verification

- `pnpm exec vitest run server/src/__tests__/agent-skills-routes.test.ts
server/src/__tests__/codex-local-skill-injection.test.ts
server/src/__tests__/codex-local-skill-sync.test.ts
server/src/__tests__/llms-routes.test.ts
server/src/__tests__/paperclip-skill-utils.test.ts --config
server/vitest.config.ts` passed: 5 files, 23 tests.
- `git diff --check public-gh/master..pap-2228-create-agent-governance
-- . ':(exclude)ui/storybook-static'` passed.
- Confirmed this PR does not include `pnpm-lock.yaml`.

## Risks

- Low-to-medium risk: this primarily changes skills/docs and tests, but
it affects future hiring guidance and approval expectations.
- Reviewers should check whether the new Security Engineer template is
too broad for default company installs.
- No database migrations.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, with shell, git, Paperclip
API, and GitHub CLI tool use in the local Paperclip workspace.

## 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)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Note: screenshot checklist item is not applicable; this PR changes
skills, docs, and server tests.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-24 14:15:28 -05:00

129 lines
4.5 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
listCodexSkills,
syncCodexSkills,
} from "@paperclipai/adapter-codex-local/server";
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
describe("codex local skill sync", () => {
const paperclipKey = "paperclipai/paperclip/paperclip";
const createAgentKey = "paperclipai/paperclip/paperclip-create-agent";
const cleanupDirs = new Set<string>();
afterEach(async () => {
await Promise.all(Array.from(cleanupDirs).map((dir) => fs.rm(dir, { recursive: true, force: true })));
cleanupDirs.clear();
});
it("reports configured Paperclip skills for workspace injection on the next run", async () => {
const codexHome = await makeTempDir("paperclip-codex-skill-sync-");
cleanupDirs.add(codexHome);
const ctx = {
agentId: "agent-1",
companyId: "company-1",
adapterType: "codex_local",
config: {
env: {
CODEX_HOME: codexHome,
},
paperclipSkillSync: {
desiredSkills: [paperclipKey],
},
},
} as const;
const before = await listCodexSkills(ctx);
expect(before.mode).toBe("ephemeral");
expect(before.desiredSkills).toContain(paperclipKey);
expect(before.desiredSkills).toContain(createAgentKey);
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 === createAgentKey)?.required).toBe(true);
expect(before.entries.find((entry) => entry.key === createAgentKey)?.state).toBe("configured");
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 () => {
const codexHome = await makeTempDir("paperclip-codex-skill-prune-");
cleanupDirs.add(codexHome);
const configuredCtx = {
agentId: "agent-2",
companyId: "company-1",
adapterType: "codex_local",
config: {
env: {
CODEX_HOME: codexHome,
},
paperclipSkillSync: {
desiredSkills: [paperclipKey],
},
},
} as const;
const after = await syncCodexSkills(configuredCtx, [paperclipKey]);
expect(after.mode).toBe("ephemeral");
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
await expect(fs.lstat(path.join(codexHome, "skills", "paperclip"))).rejects.toMatchObject({
code: "ENOENT",
});
});
it("keeps required bundled Paperclip skills configured even when the desired set is emptied", async () => {
const codexHome = await makeTempDir("paperclip-codex-skill-required-");
cleanupDirs.add(codexHome);
const configuredCtx = {
agentId: "agent-2",
companyId: "company-1",
adapterType: "codex_local",
config: {
env: {
CODEX_HOME: codexHome,
},
paperclipSkillSync: {
desiredSkills: [],
},
},
} as const;
const after = await syncCodexSkills(configuredCtx, []);
expect(after.desiredSkills).toContain(paperclipKey);
expect(after.desiredSkills).toContain(createAgentKey);
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
expect(after.entries.find((entry) => entry.key === createAgentKey)?.state).toBe("configured");
});
it("normalizes legacy flat Paperclip skill refs before reporting configured state", async () => {
const codexHome = await makeTempDir("paperclip-codex-legacy-skill-sync-");
cleanupDirs.add(codexHome);
const snapshot = await listCodexSkills({
agentId: "agent-3",
companyId: "company-1",
adapterType: "codex_local",
config: {
env: {
CODEX_HOME: codexHome,
},
paperclipSkillSync: {
desiredSkills: ["paperclip"],
},
},
});
expect(snapshot.warnings).toEqual([]);
expect(snapshot.desiredSkills).toContain(paperclipKey);
expect(snapshot.desiredSkills).not.toContain("paperclip");
expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
expect(snapshot.entries.find((entry) => entry.key === "paperclip")).toBeUndefined();
});
});