From f89813d005c11d40afa37f466bf47111e9186711 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:01:06 -0600 Subject: [PATCH] fix(api-catalog): describe the real backup and restore paths for agents (#20159) 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 > - Instance admins and operators own database backups; a chat agent cannot create one > - When a person asks an agent to back up the instance and check a restore, the agent must say who can do it and how > - The agent reads the API catalog to find the operator path, but the backup entry had only the summary "Trigger a database backup" > - With no real path in the catalog, the agent told the person to use an "Instance settings > Backups" page that does not exist > - This pull request describes the real paths in that catalog entry: the endpoint, the CLI, the retention setting, the result file, and how to check a restore safely > - The benefit is that the agent sends the person to paths that exist and keeps a restore check off the live database ## Linked Issues or Issue Description **What happened** A person asked the CEO chat agent: "Back up this test instance and check that we can restore it." The agent got 403 on `POST /api/instance/database-backups`, which is correct. It searched the catalog for backup and restore operations and found only that endpoint with no description. Its reply then said: "Create a backup in Instance settings → Backups". The web UI has no such page or control. **Expected behavior** The agent says that an instance admin or operator must do this. It names real paths: the endpoint for an instance admin, `paperclipai db:backup` on the server host, and the Backup retention setting for automatic backups. For the restore check, it says to restore into a separate empty database, not over the live one. **Steps to reproduce** 1. Start a local instance and open a chat with the CEO agent. 2. Ask: "Back up this test instance and check that we can restore it." 3. Read the reply: it names a backup page in Instance settings. Look for it in the UI: there is none. ## What Changed - `server/src/routes/openapi.ts`: add a description to `POST /api/instance/database-backups`. It says that only instance admins can call it (agents get 403, cloud-managed instances run backups for you), that automatic backups run about hourly with retention set on the company-prefixed /company/settings page, that the UI has no create or restore control, that an operator can run `paperclipai db:backup`, what the result file is, and that a restore check uses psql into a separate empty database, never over the live one. - `server/src/routes/openapi.ts`: register the success status as `201`, which is what the route returns, instead of `200`. The published document already showed `201` through the created-operations fixup, so this makes the source match it. - `server/src/__tests__/database-backup-catalog-guidance.test.ts`: new regression test. It runs the agent's catalog searches from the failed run and checks the description. It also checks that the published document shows `201` and not `200` for a successful backup. ## Verification - `pnpm exec vitest run src/__tests__/database-backup-catalog-guidance.test.ts src/__tests__/openapi-routes.test.ts` (from `server/`): 40 tests pass. Without the change, the 2 search tests fail. The status test guards the published document, which the fixup already kept correct. - `pnpm exec vitest run src/__tests__/instance-database-backups-routes.test.ts`: 7 tests pass. The server typecheck passes. - Chat eval `instance-operations.backup-and-restore` (runner-opencode), with this commit cherry-picked onto the eval branch: before the change, the reply named an "Instance settings → Backups" page that does not exist. After the change (GitHub Actions run 42552777052), the eval passed. The first reply said the agent got 403, named `POST /api/instance/database-backups` for an instance admin and `paperclipai db:backup --json` for an operator, linked Backup retention on the company settings page, and said to restore with psql into a separate, empty database, never over the live one. The second reply reported the operator's restore receipt and its limits correctly. ## Risks - Low risk. The change adds catalog text and makes the registered success status match the published 201. It does not change any API or permission behavior. - If a backup page or restore command is added later, this text must change. The regression test names these facts, so such a change fails the test and shows the text to update. > 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`. ## 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 --- .../database-backup-catalog-guidance.test.ts | 24 +++++++++++++++++++ server/src/routes/openapi.ts | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/database-backup-catalog-guidance.test.ts diff --git a/server/src/__tests__/database-backup-catalog-guidance.test.ts b/server/src/__tests__/database-backup-catalog-guidance.test.ts new file mode 100644 index 00000000000..fe47d774c2f --- /dev/null +++ b/server/src/__tests__/database-backup-catalog-guidance.test.ts @@ -0,0 +1,24 @@ +import { expect, it } from "vitest"; +import { buildOpenApiDocument } from "../routes/openapi.js"; +import { searchRunnerApi } from "../services/native-runtime/runner-api-catalog.js"; + +// An agent asked to back up and verify a restore cannot do either. Its API search must give it the real operator +// paths, so it does not invent a backup page: the retention setting, the CLI, and restore into a separate database. +it.each(["backup instance restore snapshot", "database backups list restore"])("search %j describes the real backup and restore paths", query => { + const operation = searchRunnerApi({ query, limit: 10 }).results.find(row => row.operationId === "POST /api/instance/database-backups"); + expect(operation, query).toBeDefined(); + const description = operation!.description; + expect(description).toContain("agent credentials receive 403"); + expect(description).toContain("The web UI has no control that creates or restores a backup"); + expect(description).toContain("Backup retention section of the company-prefixed /company/settings page"); + expect(description).toContain("paperclipai db:backup"); + expect(description).toContain("separate, empty PostgreSQL database, never over the live one"); +}); + +// The route answers a successful manual backup with 201. This guards the published document, which agents and +// clients read; the registration and the CREATED_OPERATIONS fixup in openapi.ts both supply that 201. +it("publishes the status the backup route returns on success", () => { + const responses = buildOpenApiDocument().paths["/api/instance/database-backups"].post.responses; + expect(Object.keys(responses)).toContain("201"); + expect(Object.keys(responses)).not.toContain("200"); +}); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 8535b74e30f..b3a9bb92797 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -9886,7 +9886,8 @@ registry.registerPath({ path: "/api/instance/database-backups", tags: ["instance"], summary: "Trigger a database backup", - responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden }, + description: "Instance admins only: agent credentials receive 403, and cloud-managed instances run backups for you. Automatic gzip-compressed backups run about hourly by default; instance admins set how long they are kept in the Backup retention section of the company-prefixed /company/settings page. The web UI has no control that creates or restores a backup. For a backup now, an instance admin calls this endpoint, or an operator with shell access to the server runs `paperclipai db:backup` (add --json for metadata). The result names a gzip-compressed SQL file (backupFile) on the server host and its size. Paperclip has no restore endpoint or command: to check a backup, an operator restores the file with psql into a separate, empty PostgreSQL database, never over the live one, and then checks the restored data.", + responses: { 201: r.ok(), 401: r.unauthorized, 403: r.forbidden }, }); // ─── LLM text endpoints ───────────────────────────────────────────────────────