From e6f0c3b521feac77b591c91acf104e2bba74d5c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 07:04:41 +0000 Subject: [PATCH] docs(spec): the AI slot answers 501, not 404, when no AI service is mounted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/spec/src/api/protocol.zod.ts`'s AI Operations note said the dispatcher "404s \"AI service is not configured\"" when nothing serves the slot. It answers 501 via the shared `capabilityUnavailable` exit, and the quoted body is no longer a local string — it comes from the shared `serviceUnavailableMessage`, so the 501 body and `discovery.services.ai` cannot drift. The replacement carries the same three arms the other three live sites gained in PR #17844: anonymous -> 401 first, `GET /ai/agents` -> 200 with an empty list as a console courtesy, every other `/ai/*` route -> 501. All three measured against `domains/ai-anonymous-deny-ordering.test.ts` (13/13 passing), not copied from the card. Prose only: no schema key moves, no accept set changes, no runtime edit. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .changeset/17847-ai-slot-501-not-404.md | 21 +++++++++++++++++++++ packages/spec/src/api/protocol.zod.ts | 16 +++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 .changeset/17847-ai-slot-501-not-404.md diff --git a/.changeset/17847-ai-slot-501-not-404.md b/.changeset/17847-ai-slot-501-not-404.md new file mode 100644 index 00000000000..c887eed540f --- /dev/null +++ b/.changeset/17847-ai-slot-501-not-404.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): the AI Operations note said the slot 404s when no AI service is mounted — it has answered 501 since the shared `capabilityUnavailable` exit landed (#17847) + +Clause-②: no — prose only. No schema key moves, no accept set widens or narrows, no export changes, and no runtime behaviour is touched; `packages/runtime` is not in this diff. + +`src/api/protocol.zod.ts` ships inside this package (`files[]` carries `src/**/*.zod.ts`, and `npm pack --dry-run` lists `src/api/protocol.zod.ts` among its 2016 entries), so the sentence an author reads is a published byte. It said: + +> this repo's dispatcher only proxies `/api/v1/ai/**` to whatever `buildAIRoutes()` mounted, or 404s "AI service is not configured" + +Both halves were stale. `packages/runtime/src/domains/ai.ts` reaches the shared `capabilityUnavailable(deps, 'ai')` exit, which answers **501 Not Implemented** — `/ai/*` IS mounted, so the request reaches a handler with nothing behind it, and 404 would claim the path does not exist. And the quoted body is no longer a local string: it comes from the shared `serviceUnavailableMessage`, the same sentence `discovery.services.ai` reports for the slot, so the 501 body and the discovery entry cannot drift into naming different remedies. The literal `AI service is not configured` survived nowhere in the tree except in that stale comment. + +The replacement is the same three-arm text the other three live sites carry after #16211 / PR #17844 (`packages/client/src/index.ts`, `packages/runtime/src/route-ledger.ts`, `packages/runtime/src/domains/ai.ts`), because an unqualified "`/ai/*` answers 501" would manufacture a second inaccurate statement: + +- an **anonymous** caller is refused **401** first (`ANONYMOUS_DENY_STATUS`), ahead of the slot being consulted — neither the 501 nor the courtesy below is owed to a caller who has not authenticated; +- **`GET /ai/agents` answers 200** with an empty list (`{ agents: [] }` under the envelope's `data`) — a deliberate console courtesy, so polling does not log an error on every navigation; +- every other `/ai/*` route answers **501** carrying the shared remedy sentence. + +All three arms were measured rather than copied: `packages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts` pins each of them and passes 13/13 on this tree. diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 78496f3f29f..98faecbf8c9 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -2788,9 +2788,19 @@ export const MarkAllNotificationsReadResponseSchema = lazySchema(() => z.object( // // The AI service itself is a Cloud/EE package (`service-ai`, in the `cloud` // repo) — this repo's dispatcher only proxies `/api/v1/ai/**` to whatever -// `buildAIRoutes()` mounted, or 404s "AI service is not configured". So these -// schemas deliberately describe **the wire**, not a protocol this repo serves: -// they are the one shape `client.ai.*` and cloud's route handlers both read. +// `buildAIRoutes()` mounted. With the service absent (the open-source +// default) the mount stays, so a request reaches a handler with nothing +// behind it and the answer is **501**, not 404 — 404 would mean the path does +// not exist, which for `/ai/*` is false. Two arms are narrower, and a caller +// branching on status needs both: an **anonymous** caller is refused **401** +// first (the 501 and the courtesy below are both capability disclosures), and +// `GET /ai/agents` answers **200** with an empty list (`{ agents: [] }` under +// the envelope's `data`) — a deliberate console courtesy. The 501 body is no +// local string: it comes from the shared `serviceUnavailableMessage`, the +// same sentence `discovery.services.ai` reports for the slot, so the two +// cannot drift into naming different remedies. So these schemas deliberately +// describe **the wire**, not a protocol this repo serves: they are the one +// shape `client.ai.*` and cloud's route handlers both read. // The mounted table's reviewed dispositions live in `cloud` // (`packages/service-ai/src/ai-route-ledger.ts`), which drives this SDK // against the routes it really returns.