diff --git a/server/src/__tests__/openapi-routes.test.ts b/server/src/__tests__/openapi-routes.test.ts index 639e8ea232f..758766bebe4 100644 --- a/server/src/__tests__/openapi-routes.test.ts +++ b/server/src/__tests__/openapi-routes.test.ts @@ -390,6 +390,26 @@ describe("openapi routes", () => { for (const code of ["200", "400", "401", "403", "404"]) expect(operation.responses[code]).toBeDefined(); }); + it("documents task-tree controls as board-only and names the human page", () => { + const spec = buildOpenApiSpec() as any; + const boardSecurity = [{ BoardSessionAuth: [] }, { BoardApiKeyAuth: [] }]; + for (const [path, method] of [ + ["/api/issues/{id}/tree-control/state", "get"], + ["/api/issues/{id}/tree-control/preview", "post"], + ["/api/issues/{id}/tree-holds", "get"], + ["/api/issues/{id}/tree-holds", "post"], + ["/api/issues/{id}/tree-holds/{holdId}", "get"], + ["/api/issues/{id}/tree-holds/{holdId}/release", "post"], + ] as const) { + const operation = spec.paths[path][method]; + expect(operation.security).toEqual(boardSecurity); + expect(operation["x-paperclip-authorization"]).toEqual({ actor: "board" }); + } + const create = spec.paths["/api/issues/{id}/tree-holds"].post.description as string; + expect(create).toContain("Pause subtree"); + expect(create).toContain("/{prefix}/issues/{identifier}"); + }); + it("documents owned conversation reads and exact revision access separately", () => { const spec = buildOpenApiSpec() as any; const boardSecurity = [{ BoardSessionAuth: [] }, { BoardApiKeyAuth: [] }]; diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index c4344bb563b..cb274455174 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -1581,6 +1581,12 @@ const BOARD_ONLY_OPERATIONS = new Set([ "POST /api/chat-identity-links/request-access", "POST /api/chat-endpoints/{endpointId}/publications/{publicationId}/resolve", "POST /api/chat-endpoints/{endpointId}/actions/{actionId}/resolve", + "GET /api/issues/{id}/tree-control/state", + "POST /api/issues/{id}/tree-control/preview", + "GET /api/issues/{id}/tree-holds", + "POST /api/issues/{id}/tree-holds", + "GET /api/issues/{id}/tree-holds/{holdId}", + "POST /api/issues/{id}/tree-holds/{holdId}/release", ]); const INSTANCE_ADMIN_OPERATIONS = new Set([ @@ -8544,6 +8550,7 @@ registry.registerPath({ path: "/api/issues/{id}/tree-holds", tags: ["issues"], summary: "Create an issue tree hold", + description: "Board-only. An agent cannot pause, cancel or resume a task tree, and this denial is final for agents. A person pauses the parent task and all of its subtasks on the company-prefixed task page /{prefix}/issues/{identifier}: More task actions, then Pause subtree. The person resumes it later on the same page with Resume subtree. Link that task page. Moving tasks to backlog or changing assignees is not a tree pause.", request: { params: z.object({ id: z.string() }), body: jsonBody(createIssueTreeHoldSchema),