From 81f12c21ecac11c97fe2c8fb8e65bfc3df0e8c02 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:35:16 -0600 Subject: [PATCH] fix(server): document the safe company import request body in the API catalog (#19968) 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 preview and apply a company package import through `POST /api/companies/{companyId}/imports/preview` and `/imports/apply` > - Agents find API operations and their request bodies in the OpenAPI catalog > - The catalog entries for these two safe import routes have no request body and no description of the safe-route limits > - An agent then offered the person a `replace` choice that the safe route rejects > - This pull request registers the existing preview and import body schemas and describes the limits > - The benefit is that agents can resolve name conflicts with `rename`, `skip` and `nameOverrides`, and do not offer choices that fail ## Linked Issues or Issue Description **What happened** A person attached a company package in chat and asked a CEO agent what the package would add. The package had an agent named Morgan, and the company already had a Morgan. The agent previewed the import correctly. It then offered three choices: `skip`, `rename`, or `replace` ("overwrites the existing Morgan"). The safe import route rejects `replace`. The catalog entries for `/imports/preview` and `/imports/apply` have no request body, so the agent could not see the route's fields or limits. PR #19945 fixed the same gap for `/exports`. **Expected behavior** The catalog documents the import body, including `source`, `target`, `collisionStrategy` and `nameOverrides`. It states that the safe route rejects `replace` and never overwrites an existing agent. **Steps to reproduce** 1. Read `GET /api/openapi.json`, `paths["/api/companies/{companyId}/imports/preview"].post`. 2. See that it has no `requestBody`. The legacy `/api/companies/import/preview` entry has one. 3. Call the safe route with `collisionStrategy: "replace"`. It returns 403. ## What Changed - Register `companyPortabilityPreviewSchema` and `companyPortabilityImportSchema` as the JSON request bodies of `POST /api/companies/{companyId}/imports/preview` and `/imports/apply` in `server/src/routes/openapi.ts`. - Add a description of the safe-route limits: target, allowed collision strategies, and `nameOverrides`. - 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 (37 tests). The new test fails on master because the entries have no `requestBody`. - An end-to-end check runs the `companies.import-preview` chat eval with this commit cherry-picked onto the eval branch. ## Risks - Low risk. This changes only API documentation. The routes already parse these schemas. Runtime behavior does not change. ## 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 | 10 ++++++++++ server/src/routes/openapi.ts | 12 ++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/openapi-routes.test.ts b/server/src/__tests__/openapi-routes.test.ts index e70b913ba2d..3500aa68f94 100644 --- a/server/src/__tests__/openapi-routes.test.ts +++ b/server/src/__tests__/openapi-routes.test.ts @@ -340,6 +340,16 @@ describe("openapi routes", () => { expect(operation.description).toMatch(/include\.projects/); } }); + it("documents the safe company import body so agents can preview and resolve agent name conflicts", () => { + const spec = buildOpenApiSpec(); + for (const path of ["/api/companies/{companyId}/imports/preview", "/api/companies/{companyId}/imports/apply"]) { + const operation = spec.paths[path].post; + const body = operation.requestBody.content["application/json"].schema; + expect(body.required).toEqual(expect.arrayContaining(["source", "target"])); + expect(body.properties).toMatchObject({ collisionStrategy: expect.anything(), nameOverrides: expect.anything(), include: expect.anything() }); + expect(operation.description).toMatch(/replace/); + } + }); 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 82c03636034..548232c49ff 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -2213,7 +2213,11 @@ registry.registerPath({ path: "/api/companies/{companyId}/imports/preview", tags: ["companies"], summary: "Preview company import", - request: { params: z.object({ companyId: z.string() }) }, + description: "Safe import into this company only: target must be { mode: \"existing_company\", companyId } for the route company or a new company, and collisionStrategy may be \"rename\" or \"skip\" (\"replace\" is rejected, so an existing agent is never overwritten). nameOverrides maps an incoming agent, project or issue slug to the name it should get. Preview never changes the company; apply creates or reuses resources and returns them.", + request: { + params: z.object({ companyId: z.string() }), + body: jsonBody(companyPortabilityPreviewSchema), + }, responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized }, }); @@ -2222,7 +2226,11 @@ registry.registerPath({ path: "/api/companies/{companyId}/imports/apply", tags: ["companies"], summary: "Apply company import", - request: { params: z.object({ companyId: z.string() }) }, + description: "Safe import into this company only: target must be { mode: \"existing_company\", companyId } for the route company or a new company, and collisionStrategy may be \"rename\" or \"skip\" (\"replace\" is rejected, so an existing agent is never overwritten). nameOverrides maps an incoming agent, project or issue slug to the name it should get. Preview never changes the company; apply creates or reuses resources and returns them.", + request: { + params: z.object({ companyId: z.string() }), + body: jsonBody(companyPortabilityImportSchema), + }, responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized }, });