From 468f22ae4b6e87b98bc9fcfcabe7e971d1f6cf02 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:55:44 -0600 Subject: [PATCH] fix(server): document the company export request body in the API catalog (#19945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A CEO agent can export the company setup through `POST /api/companies/{companyId}/exports` > - Agents find API operations and their request bodies in the OpenAPI catalog > - The catalog entries for `/exports` and `/exports/preview` have no request body > - The export defaults include only the company and agents, so an agent that sends no body gets a package without projects or skills > - This pull request registers the existing export body schema on both entries and describes the `include` defaults > - The benefit is that agents can request a complete reusable setup and do not tell users that resources do not exist ## Linked Issues or Issue Description **What happened** A user asked a CEO agent in chat: "Export this company's setup so I can reuse it." The company had an agent, a project, and a company skill. The agent looked up `POST /api/companies/{companyId}/exports` in the API catalog. Its reasoning said that the operation "has no request body defined". It sent no body. The package had only the company and the agent. The agent then told the user that the company had "no projects, issues, or company skills, so nothing else was left out." That statement was false. **Expected behavior** The catalog documents the export body, including `include.projects` and `include.skills`. An agent can request a full setup export. The legacy `/export` route already documents this body. **Steps to reproduce** 1. Create a company with a project and a company skill. 2. Read `GET /api/openapi.json`, `paths["/api/companies/{companyId}/exports"].post`. 3. See that it has no `requestBody`. The legacy `/export` entry has one. 4. Call `POST /exports` with no body. The package has no projects or skills. ## What Changed - Register `companyPortabilityExportSchema` as the request body of `POST /api/companies/{companyId}/exports` and `/exports/preview` in `server/src/routes/openapi.ts`. - Add a description that states the `include` defaults and tells callers to set `include.projects` and `include.skills` for a reusable setup. - Add a regression test in `server/src/__tests__/openapi-routes.test.ts`. ## Verification - `cd server && pnpm exec vitest run src/__tests__/openapi-routes.test.ts` passes (36 tests). The new test fails on master because the entries have no `requestBody`. - An end-to-end check runs the `companies.export-company` chat eval with this commit cherry-picked onto the eval branch. ## Risks - Low risk. This changes only API documentation. The routes already parse this schema. Runtime behavior does not change. - `imports/preview` and `imports/apply` also register no body. This PR does not change them. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution in Claude Code. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- server/src/__tests__/openapi-routes.test.ts | 9 +++++++++ server/src/routes/openapi.ts | 12 ++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/openapi-routes.test.ts b/server/src/__tests__/openapi-routes.test.ts index 9dcba27a991..e70b913ba2d 100644 --- a/server/src/__tests__/openapi-routes.test.ts +++ b/server/src/__tests__/openapi-routes.test.ts @@ -331,6 +331,15 @@ function loadSpecRoutes() { } describe("openapi routes", () => { + it("documents the export include body so agents can request projects and skills", () => { + const spec = buildOpenApiSpec(); + for (const path of ["/api/companies/{companyId}/exports", "/api/companies/{companyId}/exports/preview"]) { + const operation = spec.paths[path].post; + const body = operation.requestBody.content["application/json"].schema; + expect(body.properties.include.properties).toMatchObject({ projects: expect.anything(), skills: expect.anything() }); + expect(operation.description).toMatch(/include\.projects/); + } + }); it("documents generic webhook provenance headers and accepted responses without a provider-specific endpoint", () => { const spec = buildOpenApiSpec(); const operation = spec.paths["/api/routine-triggers/public/{publicId}/fire"].post; diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index d6745237251..405bfa1f99f 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -2187,7 +2187,11 @@ registry.registerPath({ path: "/api/companies/{companyId}/exports", tags: ["companies"], summary: "Export company data", - request: { params: z.object({ companyId: z.string() }) }, + description: "Omitting include exports only the company and agents (projects, skills and issues default to false); set include.projects and include.skills for a reusable company setup.", + request: { + params: z.object({ companyId: z.string() }), + body: jsonBody(companyPortabilityExportSchema), + }, responses: { 200: r.ok(), 401: r.unauthorized }, }); @@ -2196,7 +2200,11 @@ registry.registerPath({ path: "/api/companies/{companyId}/exports/preview", tags: ["companies"], summary: "Preview company export", - request: { params: z.object({ companyId: z.string() }) }, + description: "Omitting include exports only the company and agents (projects, skills and issues default to false); set include.projects and include.skills for a reusable company setup.", + request: { + params: z.object({ companyId: z.string() }), + body: jsonBody(companyPortabilityExportSchema), + }, responses: { 200: r.ok(), 401: r.unauthorized }, });