From 4afdf0493d2df1ceced2614f69eabddc21ff5cfe Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:55:48 -0600 Subject: [PATCH] fix(api-catalog): name the Connections page for company model service settings (#20152) 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 company can turn on model services such as Fast response, Decision model and Prompt Injection Prevention > - Only people who manage connections can change these services, so a chat agent must send the person to the right page > - The agent finds these operations through the API catalog search, but their catalog entries named no page > - The agent searched for the settings page, found nothing, and linked the general /company/settings page, which does not have the control > - This pull request adds a description to these catalog entries that names the section and the company-prefixed /company/settings/connections page > - The benefit is that the agent links the page that actually holds the control ## Linked Issues or Issue Description **What happened** A person asked the CEO chat agent: "Help me enable the company's Fast response feature so chat acknowledges my requests faster." The agent correctly got 403 on the fast-response API. It then searched the API catalog for "company settings page fast responses web UI" and found no page. It linked the general company settings page (`//company/settings`) and told the person to find Fast response there. That page has no Fast response section. The section is on /company/settings/connections. **Expected behavior** The agent links //company/settings/connections and tells the person to use the Fast response section there. If the section is hidden, the agent can say that an instance admin turns the experiment on. **Steps to reproduce** 1. Turn on the fast responses experiment. Create a shared OpenRouter API connection. 2. Open a chat with the CEO agent and ask it to enable Fast response. 3. Read the reply: it links //company/settings. Open that link: the page has no Fast response control. ## What Changed - `server/src/routes/openapi.ts`: add one shared description to the fast-response, decision-model and prompt-injection-prevention catalog entries. It names the section, the company-prefixed /company/settings/connections page, the Markdown link form, and that agent credentials cannot change or test the service. For Fast response, it also names the instance experiment that hides the section. - `server/src/__tests__/company-model-service-settings-guidance.test.ts`: new regression test. It runs the agent's catalog search (including the query from the failed run) and checks that the results name the Connections page and section. It also requires every page the descriptions name to be a route that `ui/src/App.tsx` mounts, so renaming a settings route fails CI. ## Verification - `pnpm exec vitest run src/__tests__/company-model-service-settings-guidance.test.ts` (from `server/`): 6 tests pass. Without the catalog change, the 5 search tests fail. If the Connections route in `ui/src/App.tsx` is renamed, the route test fails. - `pnpm exec vitest run src/__tests__/openapi-routes.test.ts`: 37 tests pass. - Chat eval `companies.fast-responses` (runner-opencode), with this commit cherry-picked onto the eval branch: before the change, the reply linked the general settings page, which has no Fast response control. After the change (GitHub Actions run 42549422681), the eval passed. The reply linked the Experimental settings page and the Connections page and said to use the Fast response section there. The scripted person clicked the reply's links and found the Fast response control on the Connections page, then saved and tested it. ## Risks - Low risk. The change adds catalog text only. It does not change any API or permission behavior. - If the settings page moves, these descriptions become wrong. The regression test compares every page the descriptions name with the routes in `ui/src/App.tsx`, so a route change that does not also update the description fails CI. > 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 --- ...ny-model-service-settings-guidance.test.ts | 40 +++++++++++++++++++ server/src/routes/openapi.ts | 13 +++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/company-model-service-settings-guidance.test.ts diff --git a/server/src/__tests__/company-model-service-settings-guidance.test.ts b/server/src/__tests__/company-model-service-settings-guidance.test.ts new file mode 100644 index 00000000000..ae66a624877 --- /dev/null +++ b/server/src/__tests__/company-model-service-settings-guidance.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; +import { runnerApiCatalog, searchRunnerApi } from "../services/native-runtime/runner-api-catalog.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); + +// An agent asked to turn on a company model service cannot do it and must link the page that holds the control. +// The general /company/settings page has none of these sections, so the catalog names the Connections page. +it.each([ + ["fast response feature", "/api/companies/{companyId}/fast-response", "Fast response"], + ["company settings page fast responses web UI", "/api/companies/{companyId}/fast-response", "Fast response"], + ["decision model settings", "/api/companies/{companyId}/decision-model", "Decision model"], + ["prompt injection prevention settings", "/api/companies/{companyId}/prompt-injection-prevention", "Prompt Injection Prevention"], +])("search %j returns the settings page that holds the %s control", (query, path, section) => { + const operation = searchRunnerApi({ query, limit: 10 }).results.find(row => row.path === path && row.method === "PUT"); + expect(operation, query).toBeDefined(); + expect(operation!.description).toContain(`${section} section of the company-prefixed /company/settings/connections page`); + expect(operation!.description).toContain("//company/settings/connections"); +}); + +it("names the experiment that hides the Fast response section", () => { + const operation = searchRunnerApi({ query: "fast response", limit: 10 }).results.find(row => row.operationId === "GET /api/companies/{companyId}/fast-response"); + expect(operation?.description).toContain("//company/settings/instance/experimental"); +}); + +// The catalog text is the only place these page paths live on the server, so a renamed UI route +// would leave agents linking a page that no longer exists. Every page the guidance names must be +// a route the board UI mounts under the company prefix. +it("names only pages that the board UI mounts", () => { + const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8"); + const pages = new Set( + runnerApiCatalog() + .filter(row => /^\/api\/companies\/\{companyId\}\/(fast-response|decision-model|prompt-injection-prevention)/.test(row.path)) + .flatMap(row => [...(row.description ?? "").matchAll(/\/\/([a-z0-9/-]+)/g)].map(match => match[1]!)), + ); + expect([...pages].sort()).toEqual(["company/settings/connections", "company/settings/instance/experimental"]); + for (const page of pages) expect(appRoutes, page).toContain(`/company/settings/connections, not the general /company/settings page.${condition} Agent credentials cannot change or test it.`; +} + for (const [method, suffix, summary] of [ ["get", "", "Get prompt injection settings and eligible shared OpenRouter connections; non-managers receive no settings or choices"], ["put", "", "Configure opt-in prompt injection model checks as a board connection manager"], @@ -11513,7 +11518,7 @@ for (const [method, suffix, summary] of [ ["post", "/test", "Classify two fixed billed samples using saved settings as a board connection manager; fallback is not a passing model test"], ] as const) registerCurrentRoute({ method, path: `/api/companies/{companyId}/prompt-injection-prevention${suffix}`, - tags: ["prompt-injection-prevention"], summary, + tags: ["prompt-injection-prevention"], summary, description: companyModelServiceSettingsPage("Prompt Injection Prevention"), ...(method === "put" ? { body: updatePromptInjectionPreventionSchema } : {}), ...(method === "post" ? { body: z.object({}).strict() } : {}), ...(suffix === "/models" ? { query: z.object({ connectionId: z.string().uuid() }) } : {}), @@ -11528,6 +11533,7 @@ for (const [method, suffix, summary] of [ ["post", "/test", "Generate a fixed billed fast response sample"], ["get", "/history", "List fast response accounting and publication outcomes"], ] as const) registerCurrentRoute({ method, path: `/api/companies/{companyId}/fast-response${suffix}`, tags: ["fast-responses"], summary, + description: companyModelServiceSettingsPage("Fast response", " The section is shown only while an instance admin has fast responses turned on in //company/settings/instance/experimental."), ...(method === "put" ? { body: updateFastResponseSchema } : {}), responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable } }); const observabilitySignalReceiptSchema = z.object({ @@ -11567,26 +11573,31 @@ registerCurrentRoute({ registerCurrentRoute({ method: "get", path: "/api/companies/{companyId}/decision-model", tags: ["decision-models"], + description: companyModelServiceSettingsPage("Decision model"), summary: "Get decision settings, compatible connections, and manager capability", responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden }, }); registerCurrentRoute({ method: "put", path: "/api/companies/{companyId}/decision-model", tags: ["decision-models"], + description: companyModelServiceSettingsPage("Decision model"), summary: "Configure the company decision model as a connection manager", body: updateDecisionModelSchema, responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable }, }); registerCurrentRoute({ method: "get", path: "/api/companies/{companyId}/decision-model/availability", tags: ["decision-models"], + description: companyModelServiceSettingsPage("Decision model"), summary: "Check local decision configuration and caller authorization without contacting a provider", responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden }, }); registerCurrentRoute({ method: "post", path: "/api/companies/{companyId}/decision-model/test", tags: ["decision-models"], + description: companyModelServiceSettingsPage("Decision model"), summary: "Run the fixed billed three-question setup test as a connection manager", responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden }, }); registerCurrentRoute({ method: "get", path: "/api/companies/{companyId}/decision-model/history", tags: ["decision-models"], + description: companyModelServiceSettingsPage("Decision model"), summary: "List decision metadata and charges with authorized task links", query: costReportQuerySchema.extend({ limit: z.coerce.number().int().min(1).max(500).optional() }), responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },