From 103b8c52c5b037cb3172a7327498f7cae19ffe26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 11:06:35 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(client,runtime):=20the=20AI=20slot=20a?= =?UTF-8?q?nswers=20501,=20not=20404=20=E2=80=94=20with=20its=20401-first?= =?UTF-8?q?=20and=20/ai/agents=20arms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three source sites still described the pre-`capabilityUnavailable` behaviour, saying `/ai/*` 404s "AI service is not configured" when no AI service is mounted. The dispatcher has answered 501 since the shared exit landed: the `/ai/*` routes are registered unconditionally, so a request reaches a handler with nothing behind it, which is 501 Not Implemented and not 404. The replacement is deliberately narrower than "`/ai/*` answers 501", because that sentence is not true either. Verified in `domains/ai.ts` against the unserveable-slot branch and its pins: - an anonymous caller is refused 401 first (ANONYMOUS_DENY_STATUS); - `GET /ai/agents` answers 200 with an empty list as a console courtesy; - every other route answers 501 via `capabilityUnavailable(deps, 'ai')`. The client docblock additionally gains the `/ai/agents` empty-list courtesy, which it never mentioned, and all three sites now say the 501 body comes from the shared `serviceUnavailableMessage` and so cannot drift from what discovery reports for the slot. Comment text only — no runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude --- packages/client/src/index.ts | 25 ++++++++++++++++++++++--- packages/runtime/src/domains/ai.ts | 12 +++++++----- packages/runtime/src/route-ledger.ts | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 177d2bead1..775119f9c3 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -6254,9 +6254,28 @@ export class ObjectStackClient { * * `service-ai` is a **Cloud/EE package in the `cloud` repo**. This repo's * dispatcher only proxies `/api/v1/ai/**` to whatever `buildAIRoutes()` - * mounted, and 404s `AI service is not configured` when the service is - * absent (the open-source default) — so treat every method here as - * plugin-provided and check `discovery.services` first. + * mounted. When the service is absent (the open-source default) those + * routes are still mounted, 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 than that, and a caller branching on status needs + * both: + * + * - An **anonymous** caller is refused **401** first. The 501 and the + * `/ai/agents` courtesy below are both capability disclosures, and + * neither is owed to a caller who has not authenticated. + * - **`GET /ai/agents` answers `200`** with an empty list (`{ agents: [] }` + * under the envelope's `data`), not 501. It is a deliberate courtesy: a + * console polls it on every navigation to decide whether to show AI + * affordances, and an empty catalog conveys "no AI service here" without + * looking like a fault. + * + * The 501 body is not a 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. Treat every method here as plugin-provided and check + * `discovery.services` first. * * That split is also why the guard for these URLs lives on the other side of * the repo boundary: `cloud`'s `packages/service-ai/src/ai-route-ledger.ts` diff --git a/packages/runtime/src/domains/ai.ts b/packages/runtime/src/domains/ai.ts index ced7e862e1..7b4fff5298 100644 --- a/packages/runtime/src/domains/ai.ts +++ b/packages/runtime/src/domains/ai.ts @@ -82,11 +82,13 @@ export async function handleAIRequest(deps: DomainHandlerDeps, subPath: string, // authenticated. This is the exit the defect lived behind. if (denyAnonymous) return anonymousRefusal(); // The console polls `GET /ai/agents` on every navigation to decide - // whether to show AI affordances. Reporting that as a 404 turns the - // normal "no AI service configured" state (the open-source default — - // service-ai is a Cloud/Enterprise package) into console error-log - // spam on every page. An empty list conveys the same information - // without looking like a fault. Every other /ai/* route still 404s. + // whether to show AI affordances. Reporting that as an error turns + // the normal "no AI service configured" state (the open-source + // default — service-ai is a Cloud/Enterprise package) into console + // error-log spam on every page. An empty list conveys the same + // information without looking like a fault. Every other /ai/* route + // answers 501 below — an anonymous caller having already been + // refused 401 at the gate above. // // The body is the declared envelope (#4053). `data` carries // `AiAgentsResponseSchema`'s `{ agents }` — a RELOCATION of the declared diff --git a/packages/runtime/src/route-ledger.ts b/packages/runtime/src/route-ledger.ts index 013030f987..b99c71fdab 100644 --- a/packages/runtime/src/route-ledger.ts +++ b/packages/runtime/src/route-ledger.ts @@ -468,7 +468,7 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ // ── ai (dynamic route table, owned by another repo) ─────────────────────── { route: '* /ai/**', domain: '/ai', disposition: 'dynamic', - note: 'routes come from service-ai buildAIRoutes() at plugin start — service-ai is a Cloud/EE package in the `cloud` repo, so this repo cannot enumerate them and the dispatcher only proxies (or 404s "AI service is not configured"). Enumerated on the other side of that boundary since #3718: cloud packages/service-ai/src/ai-route-ledger.ts, whose conformance test drives client.ai.* against the table buildAIRoutes() really returns. The client now expresses that table — ai.chat / ai.chatStream / ai.complete / ai.models / ai.conversations.* — but do NOT read a `sdk` disposition into this row: it stays `dynamic` because THIS repo still cannot see the routes. An earlier note here claimed the client "expresses nlq/suggest/insights against the REST AI routes"; that was never verified and was FALSE — nothing has ever mounted those three paths, and both they and the methods calling them are gone (#3718)' }, + note: 'routes come from service-ai buildAIRoutes() at plugin start — service-ai is a Cloud/EE package in the `cloud` repo, so this repo cannot enumerate them and the dispatcher only proxies. With the service absent the mount stays, so the answer is 501 carrying the shared serviceUnavailableMessage (the sentence discovery reports for the slot), NOT 404 — with two narrower arms: an anonymous caller is refused 401 first, and GET /ai/agents answers 200 with an empty list as a console courtesy. Enumerated on the other side of that boundary since #3718: cloud packages/service-ai/src/ai-route-ledger.ts, whose conformance test drives client.ai.* against the table buildAIRoutes() really returns. The client now expresses that table — ai.chat / ai.chatStream / ai.complete / ai.models / ai.conversations.* — but do NOT read a `sdk` disposition into this row: it stays `dynamic` because THIS repo still cannot see the routes. An earlier note here claimed the client "expresses nlq/suggest/insights against the REST AI routes"; that was never verified and was FALSE — nothing has ever mounted those three paths, and both they and the methods calling them are gone (#3718)' }, // ── meta (legacy chain) ─────────────────────────────────────────────────── // [2026-08-31] SEEDED under the field's fill rule: `anonymous-deny-meta` is From c3b4c470f835e71ca501b385a422bf631876254c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 11:15:02 +0000 Subject: [PATCH 2/2] chore(changeset): patch @objectstack/client for the AI-slot 501 docblock correction The docblock is emitted into all four of the client's built artifacts, so the published surface moves and `skip-changeset` would be wrong. Measured on the built tree with a lit control; the runtime-side siblings publish nothing and are deliberately not named. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude --- .changeset/16211-ai-slot-501-not-404.md | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .changeset/16211-ai-slot-501-not-404.md diff --git a/.changeset/16211-ai-slot-501-not-404.md b/.changeset/16211-ai-slot-501-not-404.md new file mode 100644 index 0000000000..42ffcf8ae1 --- /dev/null +++ b/.changeset/16211-ai-slot-501-not-404.md @@ -0,0 +1,64 @@ +--- +'@objectstack/client': patch +--- + +`client.ai`'s docblock says the AI slot answers 501, names the 401-first and `GET /ai/agents` arms, and stops promising a 404 + +The `ai` namespace docblock described the pre-`capabilityUnavailable` +behaviour: that this repo's dispatcher *"404s `AI service is not configured` +when the service is absent (the open-source default)"*. The dispatcher has +answered **501** since the shared exit landed. `/ai/*` is registered +**unconditionally** (`createAiDomain`, plus the host wildcard across four +methods in every branch of the scoping conditional), so a request reaches a +handler with nothing behind it — which is 501 Not Implemented, not 404. +`packages/runtime/src/domains/unavailable.ts` exists to draw exactly that line: +404 means *the route is not there*, and for `/ai/*` that is false. + +**Why the replacement is narrower than "`/ai/*` answers 501".** That sentence +is not true either, and a caller branching on status needs both exceptions. +Verified against the unserveable-slot branch in +`packages/runtime/src/domains/ai.ts`, in its own evaluation order: + +``` +FROM any /ai/* with no AI service -> 404 `AI service is not configured` + +TO anonymous caller -> 401 (ANONYMOUS_DENY_STATUS; the 501 and + the courtesy below are capability + disclosures, owed to nobody who has + not authenticated) + GET /ai/agents -> 200 { agents: [] } under the envelope's + `data` — a console polls it on every + navigation to decide whether to show + AI affordances + every other /ai/* route -> 501 serviceUnavailableMessage('ai') +``` + +All three arms are already test-pinned in +`domains/ai-anonymous-deny-ordering.test.ts` — this changeset moves no +behaviour, only the sentence describing it. + +**The `GET /ai/agents` courtesy was mentioned nowhere in this docblock**, which +is the one an SDK reader actually opens, so it is added rather than merely +corrected. Also stated now: the 501 body is not a 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. + +⛔ No behaviour changes. This is a docblock; no export, authorable key, accept +set or response byte moves. + +**This is shipped, which is why it carries a changeset rather than +`skip-changeset`.** `@objectstack/client`'s published `files[]` ships `dist`, +and this TSDoc is emitted into all four built artifacts — `dist/index.d.ts`, +`dist/index.d.mts`, `dist/index.js` and `dist/index.mjs` — measured on the +built tree, with the stale `AI service is not configured` sentence absent from +every built file afterwards and the docblock's own neighbouring sentence +present as the lit control. The declarations are what a consumer's editor shows +on hover and what an upgrading agent greps, and they change. + +The two sibling corrections in the same change do **not** publish and are not +named here: `packages/runtime/src/route-ledger.ts` is a CI-audit ledger that is +not exported from the runtime entry (`ROUTE_LEDGER` is absent from +`packages/runtime/dist` entirely), and the `domains/ai.ts` implementation +comment is not emitted — three pre-existing comments from that same file were +probed as controls and none appears in the built output.