From e7ff8ba610e26a8072e35491de8f4e220f745bd8 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 9 Nov 2026 00:11:44 -0600 Subject: [PATCH] fix(api): use a neutral catalog pointer in the call_api protocol refusal A non-empty description does not always say who can use the endpoint (the WebSocket entries only say they are not callable through call_api), so point to the description for details instead. Co-Authored-By: Claude Opus 5.5 --- .../src/__tests__/assistant-connections-guidance.test.ts | 9 ++++++++- server/src/services/native-runtime/runner-api-client.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/assistant-connections-guidance.test.ts b/server/src/__tests__/assistant-connections-guidance.test.ts index d03719da11a..563b3e04891 100644 --- a/server/src/__tests__/assistant-connections-guidance.test.ts +++ b/server/src/__tests__/assistant-connections-guidance.test.ts @@ -21,7 +21,7 @@ it("tells agents searching for assistant disconnection that only the person can it("says the call_api protocol rejection is final instead of inviting another search", () => { expect(() => validateRunnerApiCall({ operationId: "GET /api/mcp/connections" }, context)) - .toThrow(/no other API route substitutes for it\. Its catalog description says who can use it\./); + .toThrow(/no other API route substitutes for it\. Read its catalog description for details\./); }); it("points at the catalog description only when the protocol endpoint has one", () => { @@ -32,3 +32,10 @@ it("points at the catalog description only when the protocol endpoint has one", expect(message).toContain("no other API route substitutes for it."); expect(message).not.toContain("catalog description"); }); + +it("does not claim a non-guidance protocol description says who can use the endpoint", () => { + let message = ""; + try { validateRunnerApiCall({ operationId: "GET /api/companies/{companyId}/events/ws" }, context); } catch (error) { message = (error as Error).message; } + expect(message).toContain("Read its catalog description for details."); + expect(message).not.toContain("who can use it"); +}); diff --git a/server/src/services/native-runtime/runner-api-client.ts b/server/src/services/native-runtime/runner-api-client.ts index 42a1af9ad19..ecce7755c8f 100644 --- a/server/src/services/native-runtime/runner-api-client.ts +++ b/server/src/services/native-runtime/runner-api-client.ts @@ -54,7 +54,7 @@ export function validateRunnerApiCall(value: unknown, context: RunnerApiContext) if (!parsed.success) throw badRequest("Invalid call_api arguments", { issues: parsed.error.issues }); const input = parsed.data; const operation = runnerApiOperation(input.operationId); - if (operation.transport !== "rest") throw unprocessable(`This endpoint requires its existing protocol client; call_api supports REST only and no other API route substitutes for it.${operation.description.trim() ? " Its catalog description says who can use it." : ""}`); + if (operation.transport !== "rest") throw unprocessable(`This endpoint requires its existing protocol client; call_api supports REST only and no other API route substitutes for it.${operation.description.trim() ? " Read its catalog description for details." : ""}`); if (input.responseText && operation.method !== "GET") throw badRequest("responseText requires a GET operation; read the saved artifact instead of repeating a mutation"); if (!operation.allowedModes.includes(context.workMode)) throw forbidden("call_api permits only reads in Ask and Plan modes; use the permitted dedicated tools"); if (input.pathParams?.companyId && input.pathParams.companyId !== context.companyId) throw forbidden("API call belongs to another company");