Files
paperclip/server/src/routes/llms.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

88 lines
3.5 KiB
TypeScript

import { Router, type Request } from "express";
import type { Db } from "@paperclipai/db";
import { AGENT_ICON_NAMES } from "@paperclipai/shared";
import { forbidden } from "../errors.js";
import { listServerAdapters } from "../adapters/index.js";
import { agentService } from "../services/agents.js";
function hasCreatePermission(agent: { role: string; permissions: Record<string, unknown> | null | undefined }) {
if (!agent.permissions || typeof agent.permissions !== "object") return false;
return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
}
export function llmRoutes(db: Db) {
const router = Router();
const agentsSvc = agentService(db);
async function assertCanRead(req: Request) {
if (req.actor.type === "board") return;
if (req.actor.type !== "agent" || !req.actor.agentId) {
throw forbidden("Board or permitted agent authentication required");
}
const actorAgent = await agentsSvc.getById(req.actor.agentId);
if (!actorAgent || !hasCreatePermission(actorAgent)) {
throw forbidden("Missing permission to read agent configuration reflection");
}
}
router.get("/llms/agent-configuration.txt", async (req, res) => {
await assertCanRead(req);
const adapters = listServerAdapters().sort((a, b) => a.type.localeCompare(b.type));
const lines = [
"# Paperclip Agent Configuration Index",
"",
"Installed adapters:",
...adapters.map((adapter) => `- ${adapter.type}: /llms/agent-configuration/${adapter.type}.txt`),
"",
"Related API endpoints:",
"- GET /api/companies/:companyId/agent-configurations",
"- GET /api/agents/:id/configuration",
"- POST /api/companies/:companyId/agent-hires",
"",
"Agent identity references:",
"- GET /llms/agent-icons.txt",
"",
"Notes:",
"- Sensitive values are redacted in configuration read APIs.",
"- New hires may be created in pending_approval state depending on company settings.",
"- Use the paperclip-create-agent skill for end-to-end hiring: adapter reflection, config comparison, instruction source selection, icon choice, desiredSkills, sourceIssueId/sourceIssueIds, and approval follow-up.",
"- Timer heartbeats are opt-in for new hires. Leave runtimeConfig.heartbeat.enabled false unless the role truly needs scheduled work or the user explicitly asked for it.",
"",
];
res.type("text/plain").send(lines.join("\n"));
});
router.get("/llms/agent-icons.txt", async (req, res) => {
await assertCanRead(req);
const lines = [
"# Paperclip Agent Icon Names",
"",
"Set the `icon` field on hire/create payloads to one of:",
...AGENT_ICON_NAMES.map((name) => `- ${name}`),
"",
"Example:",
'{ "name": "SearchOps", "role": "researcher", "icon": "search" }',
"",
];
res.type("text/plain").send(lines.join("\n"));
});
router.get("/llms/agent-configuration/:adapterType.txt", async (req, res) => {
await assertCanRead(req);
const adapterType = req.params.adapterType as string;
const adapter = listServerAdapters().find((entry) => entry.type === adapterType);
if (!adapter) {
res.status(404).type("text/plain").send(`Unknown adapter type: ${adapterType}`);
return;
}
res
.type("text/plain")
.send(
adapter.agentConfigurationDoc ??
`# ${adapterType} agent configuration\n\nNo adapter-specific documentation registered.`,
);
});
return router;
}