[codex] Add teams catalog extraction (#7550)
Fixes #7551 ## Thinking Path > - Paperclip is the control plane for AI-agent companies, and reusable company/team setup is part of making those companies faster to launch. > - The teams catalog work introduces app-shipped team templates that can be browsed, previewed, and installed into a company. > - Catalog installation crosses several contracts: bundled package contents, shared API types, server import/install behavior, CLI workflows, and the board UI. > - Agents also need a safe path through catalog installs: scoped company selection, explicit source policy, approval fallback for agent creation, and preserved catalog provenance. > - This pull request extracts the completed teams catalog branch into one reviewable PR on top of `public-gh/master`. > - The benefit is a reusable teams catalog foundation with server, CLI, package, docs, and hidden UI surfaces kept in sync. ## What Changed - Added the `@paperclipai/teams-catalog` package with bundled/optional team definitions, generated manifest, validators, catalog builder tests, and migration notes. - Added shared teams catalog types/validators plus server routes and services for listing, previewing, and installing catalog teams. - Integrated catalog install with company portability, skill/source policy checks, provenance metadata, origin hashes, target-manager reparenting, and installed/out-of-date detection. - Added CLI `teams` commands and agent-safe company selection behavior, including `company current` and approval fallback for forbidden agent-run installs. - Added hidden Team Catalog UI/API/query surfaces, Storybook fixtures, and targeted UI tests while keeping the UI route out of primary navigation. - Added docs for CLI/company/teams catalog behavior and removed generated screenshot artifacts from the PR diff. ## Verification - `pnpm exec vitest run cli/src/__tests__/company.test.ts cli/src/__tests__/teams.test.ts packages/teams-catalog/src/catalog-builder.test.ts packages/teams-catalog/src/shipped-catalog.test.ts server/src/__tests__/agent-permissions-service.test.ts server/src/__tests__/company-portability.test.ts server/src/__tests__/company-skills-service.test.ts server/src/__tests__/teams-catalog-routes.test.ts server/src/__tests__/teams-catalog-service.test.ts server/src/__tests__/teams-catalog-install-no-overrides.test.ts ui/src/lib/company-routes.test.ts ui/src/pages/TeamCard.test.tsx ui/src/pages/TeamCatalog.test.tsx ui/src/pages/useInstallTeamCatalogEntry.test.tsx` - `pnpm --filter @paperclipai/shared typecheck && pnpm --filter @paperclipai/teams-catalog typecheck && pnpm --filter paperclipai typecheck && pnpm --filter @paperclipai/server typecheck && pnpm --filter @paperclipai/ui typecheck` - Confirmed branch is rebased onto `public-gh/master` (`78dc3625a`) and `public-gh/master` is an ancestor of `HEAD`. - Confirmed PR diff excludes `pnpm-lock.yaml`, `.github/workflows/*`, generated screenshot images, and screenshot helper scripts. ## Risks - Medium review surface: this crosses package generation, shared contracts, server install behavior, CLI, docs, and hidden UI code. - Catalog install behavior creates agents/projects/tasks/skills and must keep company scoping, permissions, source policy, and provenance checks strict. - `pnpm-lock.yaml` is intentionally excluded per repo policy; CI/default-branch automation owns lockfile refresh. - The Team Catalog UI is included but hidden from primary navigation, so future enablement should re-check visual QA before exposure. > 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`. > > ROADMAP checked: this aligns with reusable companies/templates and plugin-adjacent onboarding work. This PR packages work already developed on the Paperclip task branch for review. ## Model Used - OpenAI Codex, GPT-5 series coding agent in this Paperclip session; exact runtime context window was not exposed. Used shell, git, `gh`, and local test/typecheck tooling. ## 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, or documented why screenshots are intentionally omitted - [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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
export { healthRoutes } from "./health.js";
|
||||
export { companyRoutes } from "./companies.js";
|
||||
export { companySkillRoutes } from "./company-skills.js";
|
||||
export { teamsCatalogRoutes } from "./teams-catalog.js";
|
||||
export { agentRoutes } from "./agents.js";
|
||||
export { projectRoutes } from "./projects.js";
|
||||
export { issueRoutes } from "./issues.js";
|
||||
|
||||
@@ -871,6 +871,24 @@ registry.registerPath({
|
||||
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
||||
});
|
||||
|
||||
// ─── Teams Catalog ──────────────────────────────────────────────────────────
|
||||
|
||||
for (const route of [
|
||||
["get", "/api/teams/catalog", "List catalog teams"],
|
||||
["get", "/api/teams/catalog/{catalogId}/files", "Get catalog team file"],
|
||||
["get", "/api/teams/catalog/{catalogId}", "Get catalog team"],
|
||||
["get", "/api/companies/{companyId}/teams/catalog/installed", "List installed catalog teams"],
|
||||
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/preview", "Preview catalog team install"],
|
||||
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/install", "Install catalog team"],
|
||||
] as const) {
|
||||
registerCurrentRoute({
|
||||
method: route[0],
|
||||
path: route[1],
|
||||
tags: ["teams"],
|
||||
summary: route[2],
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Agents ──────────────────────────────────────────────────────────────────
|
||||
|
||||
registry.registerPath({
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Router, type Request } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
catalogTeamInstallSchema,
|
||||
catalogTeamListQuerySchema,
|
||||
catalogTeamPreviewSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { accessService, agentService } from "../services/index.js";
|
||||
import {
|
||||
getCatalogTeamOrThrow,
|
||||
listCatalogTeams,
|
||||
readCatalogTeamFile,
|
||||
teamsCatalogService,
|
||||
} from "../services/teams-catalog.js";
|
||||
import { forbidden } from "../errors.js";
|
||||
import { assertAuthenticated, assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
|
||||
export function teamsCatalogRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const agents = agentService(db);
|
||||
const access = accessService(db);
|
||||
const svc = teamsCatalogService(db);
|
||||
|
||||
function canCreateAgents(agent: { permissions: Record<string, unknown> | null | undefined }) {
|
||||
if (!agent.permissions || typeof agent.permissions !== "object") return false;
|
||||
return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
|
||||
}
|
||||
|
||||
function firstQueryString(value: unknown): string | undefined {
|
||||
if (typeof value === "string") return value;
|
||||
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function assertCanInstallCatalogTeam(req: Request, companyId: string) {
|
||||
assertCompanyAccess(req, companyId);
|
||||
|
||||
if (req.actor.type === "board") {
|
||||
if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) return;
|
||||
const allowed = await access.canUser(companyId, req.actor.userId, "agents:create");
|
||||
if (!allowed) {
|
||||
throw forbidden("Missing permission: agents:create");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.actor.agentId) {
|
||||
throw forbidden("Agent authentication required");
|
||||
}
|
||||
|
||||
const actorAgent = await agents.getById(req.actor.agentId);
|
||||
if (!actorAgent || actorAgent.companyId !== companyId) {
|
||||
throw forbidden("Agent key cannot access another company");
|
||||
}
|
||||
|
||||
const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgent.id, "agents:create");
|
||||
if (allowedByGrant || canCreateAgents(actorAgent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw forbidden("Missing permission: can create agents");
|
||||
}
|
||||
|
||||
router.get("/teams/catalog", async (req, res) => {
|
||||
assertAuthenticated(req);
|
||||
const query = catalogTeamListQuerySchema.parse({
|
||||
kind: firstQueryString(req.query.kind),
|
||||
category: firstQueryString(req.query.category),
|
||||
q: firstQueryString(req.query.q),
|
||||
});
|
||||
res.json(await listCatalogTeams(query));
|
||||
});
|
||||
|
||||
router.get("/teams/catalog/:catalogId/files", async (req, res) => {
|
||||
assertAuthenticated(req);
|
||||
const catalogRef = firstQueryString(req.query.ref) ?? (req.params.catalogId as string);
|
||||
const relativePath = firstQueryString(req.query.path) ?? "TEAM.md";
|
||||
res.json(await readCatalogTeamFile(catalogRef, relativePath));
|
||||
});
|
||||
|
||||
router.get("/teams/catalog/:catalogId", async (req, res) => {
|
||||
assertAuthenticated(req);
|
||||
const catalogRef = firstQueryString(req.query.ref) ?? (req.params.catalogId as string);
|
||||
res.json(await getCatalogTeamOrThrow(catalogRef));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/teams/catalog/installed", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await svc.listInstalledCatalogTeams(companyId));
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/teams/catalog/:catalogId/preview",
|
||||
validate(catalogTeamPreviewSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const catalogRef = firstQueryString(req.query.ref) ?? (req.params.catalogId as string);
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.previewCatalogTeamImport(companyId, catalogRef, {
|
||||
...req.body,
|
||||
actor: getActorInfo(req),
|
||||
});
|
||||
res.json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/teams/catalog/:catalogId/install",
|
||||
validate(catalogTeamInstallSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const catalogRef = firstQueryString(req.query.ref) ?? (req.params.catalogId as string);
|
||||
await assertCanInstallCatalogTeam(req, companyId);
|
||||
const result = await svc.installCatalogTeam(companyId, catalogRef, {
|
||||
...req.body,
|
||||
actor: getActorInfo(req),
|
||||
});
|
||||
res.status(201).json(result);
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user