From 7044ff96e3ddfa17234dc91cf8ce75e56574c5ac Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:56:53 -0600 Subject: [PATCH] fix(api-catalog): mark task-tree controls board-only and name the Pause subtree page (#20252) 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 > - Agents read the API catalog (`server/src/routes/openapi.ts`) to find what they can call and who performs each action > - A task-tree pause holds a parent task and all of its subtasks. Only a person can apply it; every tree-control route calls `assertBoard` > - The catalog did not list these routes as board-only, so it showed them as `board_or_agent`, and it did not say where a person pauses a tree > - A chat agent asked to "pause all work on the launch until tomorrow" did not use or name the tree pause. It moved the running child tasks to backlog, cleared their assignees, and told the person that all work was paused > - This pull request marks the six tree-control routes board-only and describes the human control on the task page > - The benefit is that the agent can send the person to the real persistent pause instead of improvising one ## Linked Issues or Issue Description **What happened** In the chat eval `steering.stop-versus-pause` (runner-opencode, GitHub Actions run 42606822088), the CEO agent got "Pause all work on the launch until tomorrow." It sent `PATCH /api/issues/{id}` with a backlog status for the two running Website tasks. That cleared their execution assignments. It replied "All launch work is paused." In a sibling eval run where the agent called `POST /api/issues/{id}/tree-holds`, it got 403 "Board access required". The catalog showed that route as callable by agents. **Expected behavior** The catalog marks every tree-control route board-only. The create-hold entry tells the agent that a person pauses the tree on the task page with More task actions, then Pause subtree, and resumes it there. It also says that backlog or reassignment is not a tree pause. **Steps to reproduce** 1. Run `buildOpenApiSpec()` on master. 2. Read `paths["/api/issues/{id}/tree-holds"].post["x-paperclip-authorization"]`. 3. It is `{ actor: "board_or_agent" }`, but the route calls `assertBoard`. ## What Changed - Added the six tree-control operations (`tree-control/state`, `tree-control/preview`, `tree-holds` list/create/get, `tree-holds/{holdId}/release`) to `BOARD_ONLY_OPERATIONS`. - Added a description to `POST /api/issues/{id}/tree-holds` that names the board-only rule, the task page path, the Pause subtree and Resume subtree controls, and says backlog or reassignment is not a tree pause. - Added an `openapi-routes.test.ts` case for the board-only authorization and the description. ## Verification - `pnpm exec vitest run server/src/__tests__/openapi-routes.test.ts -t "task-tree controls"` fails before the change and passes after it. - `server/src/__tests__/openapi-routes.test.ts` and `server/src/services/native-runtime/runner-api.test.ts` pass (2111 tests). - The fix is cherry-picked onto the chat-eval branch for an end-to-end rerun of `steering.stop-versus-pause` and `task-coordination.pause-tree`. ## Risks - Low risk. Catalog metadata only. The routes already reject agents, so no runtime permission changes. - Agents now see these operations as board-only. That matches the existing 403. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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 | 20 ++++++++++++++++++++ server/src/routes/openapi.ts | 7 +++++++ 2 files changed, 27 insertions(+) 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),