Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/17847-ai-slot-501-not-404.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 13 additions & 3 deletions packages/spec/src/api/protocol.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading