You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(plugin-dev,runtime): retire the dev analytics stub; /analytics gates on handlerReady, not presence (#4000) (#4062)
#3891/#3989 retired the degraded analytics shim, making an empty `analytics`
slot the honest signal: routes unmounted, request 404, discovery `unavailable`.
plugin-dev refilled that slot in dev mode and the retired shape came back one
layer down — the dispatcher gates on "is a service registered", so the stub was
called like a real engine and its fabricated rows went out as a 200. Dev mode is
explicit opt-in and the fake was empty, so this was never the security hole the
shim was; the defect is that "the capability is present" meant something
different in dev than in production.
Both halves are fixed, because deleting the stub alone would leave the gate
that let it through:
- plugin-dev no longer registers an `analytics` dev stub (`NO_DEV_STUB_SERVICES`
— the slot stays empty, not even the shapeless `{ _dev: true }` fallback).
Every other dev stub is unchanged.
- ADR-0076 D12's third conclusion ("consumers treat only `handlerReady: true` as
a real capability") is now executed by the dispatcher, which is a consumer
too. `isAnalyticsServiceServeable` is read by the `/analytics` domain, the
route-mount gate, and discovery's `routes`/`features` — one predicate, so what
is advertised and what is served cannot disagree. `handlerReady`, not
`status`, is the test: a `degraded` implementation that genuinely serves keeps
serving.
- The metadata-protocol discovery builder stops advertising `routes.analytics`
(and the service entry's `route`) for a stub, for the same reason. Analytics
only: the other stub-backed domains ARE still served by their dispatcher
domains, so their presence-gated advertisement stays honest.
- `discovery.services.analytics` still reports a registered stub as
`status: 'stub', handlerReady: false` — strictly more informative than
collapsing it to `unavailable`.
To use analytics in dev, install the real engine: @objectstack/service-analytics
runs an InMemory strategy. `os serve`/`os dev` are unaffected — `analytics` is in
ALWAYS_ON_CAPABILITIES, so the real engine is already installed there.
Out of scope, filed as #4058: the other dispatcher domains (storage, automation,
i18n, notifications, ai) still gate on presence alone, and plugin-dev's remaining
stubs still occupy those slots. Adopting the same rule there is a per-domain
call — some of those stubs are working in-memory implementations (`degraded`),
not fabricated data (`stub`), and the single `_dev: true` marker cannot tell
them apart. ADR-0076 D12 annotated accordingly.
Retire the dev-mode `analytics` stub, and make the dispatcher gate `/analytics` on `handlerReady` rather than on service presence (#4000).
7
+
8
+
Retiring the degraded analytics shim (#3891) made an empty `analytics` slot the honest signal: `/api/v1/analytics/*` 404s and discovery reports `unavailable`. `plugin-dev` refilled that slot with a stub, which re-created the retired shape in dev mode — the dispatcher gated on "is a service registered", so the stub was called like a real engine and its empty result came back as a 200.
9
+
10
+
-`plugin-dev` no longer registers an `analytics` dev stub; the slot stays empty (`NO_DEV_STUB_SERVICES`). Every other dev stub is unchanged.
11
+
- The `/analytics` domain, its route-mount gate, and discovery's `routes`/`features` now share one predicate (`isAnalyticsServiceServeable`): a service that self-declares `handlerReady: false` (ADR-0076 D12 — `__serviceInfo`, or plugin-dev's legacy `_dev: true`) is treated as an empty slot. A `degraded` implementation that genuinely serves requests keeps serving; `discovery.services.analytics` still reports a registered stub as `status: 'stub'`, which says more than `unavailable` would.
12
+
13
+
FROM → TO for dev setups that relied on the stub answering `POST /api/v1/analytics/query` with `{ rows: [], fields: [] }`: install the real engine — `@objectstack/service-analytics` runs an InMemory strategy and needs no database of its own. Nothing else changes; hosts that already install it (including `os serve`, where `analytics` is in `ALWAYS_ON_CAPABILITIES`) are unaffected.
Copy file name to clipboardExpand all lines: docs/adr/0076-objectql-core-tiering.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -132,14 +132,14 @@ Decision: each capability plugin registers its routes as a **normalized handler*
132
132
**Root cause of agents being misled.** Several plugins register stub / dev / fallback services under canonical names, and the discovery builder reports *any* present service as fully real: `runtime/http-dispatcher.ts`'s `svcAvailable` hardcodes `{ enabled: true, status: 'available', handlerReady: true }` for every registered service — it **ignores stub markers** (its own comment even says "handlerReady:false … may be served by a stub", but the code never computes it). So `discovery.services.*` claims capabilities that are only stubbed, and consumers (AI agents, the console) trust them. A dev AI stub advertised this way has already confused an agent.
-`plugin-dev` registers ~8 dev stubs — `storage` / `search` / `automation` / `graphql` / `analytics` / `realtime` / `notification` / `ai` (they already carry a `_dev: true` marker that nothing respects).*(Update #4000: the `analytics` one is **gone** — after the fallback's retirement (#3891/#3989) it was the last thing refilling that slot, and refilling it re-created the retired shape in dev: the dispatcher gated on service presence, so the stub was called like an engine and answered 200 with fabricated rows. The remaining stubs are unchanged, and the class-wide question they raise is tracked separately — see conclusion 3 below.)*
136
136
-`ObjectQLPlugin` registers a ~66-line `analytics`**fallback** (the D10 note — deliberate, but it reports as fully available).
137
137
-`http-dispatcher.ts``svcAvailable` — the hardcode above.
138
138
139
139
**Decision — honest capabilities:**
140
140
1. A registered service that is a stub / dev / fallback MUST self-identify with a **standard marker** (standardise one; `_dev` is the existing precedent).
141
141
2. Discovery MUST respect it: such services report **`status: 'stub'` (or `'degraded'`) with `handlerReady: false`**, never `status: 'available'`. Add the status value to the discovery schema.
142
-
3. Consumers (agents, console) treat **only `handlerReady: true` / `status: 'available'`** as a real capability; `stub`/`unavailable` ⇒ do not use for real work.
142
+
3. Consumers (agents, console) treat **only `handlerReady: true` / `status: 'available'`** as a real capability; `stub`/`unavailable` ⇒ do not use for real work.*(Update #4000: the **dispatcher is a consumer too**, and was not executing this — every service domain gates on "is the slot occupied", never on `handlerReady`, so a stub in the slot was called like a real implementation. Enforced for `analytics` (`isAnalyticsServiceServeable` in `runtime/src/domains/analytics.ts`, read by the domain, the route-mount gate, and `routes`/`features` in discovery — one predicate, so advertised and served cannot disagree). The other stub-backed domains — `file-storage` / `search` / `automation` / `realtime` / `notification` / `ai` / `i18n` — still gate on presence alone; adopting the same rule across them is a per-domain call, tracked in #4058.)*
143
143
144
144
This fixes the **whole class at once — without deleting any fallback** (no `/analytics` 404 regression): the analytics fallback and the dev stubs simply stop *lying*; they keep serving but are honestly labelled. It is the runtime enforcement of the D9-refinement principle (capabilities = what is actually installed, computed at runtime).
0 commit comments