Skip to content

Commit 0bb8dbd

Browse files
os-zhuangclaude
andauthored
docs(qa): record dispatcher-plugin.ts as deliberately outside the #2992 transport tripwire set (#9410) (#9522)
packages/runtime/src/dispatcher-plugin.ts mounts routes (/actions, /automation, /packages — the registration path separate from the @objectstack/rest one) and already writes SSE at two sites, yet none of the five #2992 / ADR-0096 D4 transport tripwires watch it. A subscribe/fan-out transport wired there mints no TRANSPORT-WIRED key and reds no build, and the protocol page stated that limitation only in general terms. This is a recording. No probe is added, no key is minted, and no matrix row is written for the two existing sites. The reason, recorded because a reader cannot re-derive it cheaply: both text/event-stream sites are per-request AI response streaming, not realtime subscription fan-out. Each drains one AsyncIterable returned by the route handler into that same request's response body and then calls res.end(); no subscriber is registered, no event reaches a set of recipients, and the file carries no upgrade handler, no subscribe registration and no realtime-service call. Watching it with the existing mechanics pattern would mint a key on day one for a surface the tripwires are not about. The boundary is drawn on fan-out, not on the SSE content type, and both records say so. Promoting the file into the population with a fan-out-specific marker is written into #8347's acceptance as a precondition of the WebSocket/SSE transport landing. Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 072b55a commit 0bb8dbd

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/dogfood": patch
3+
---
4+
5+
docs(qa): record `dispatcher-plugin.ts` as deliberately outside the #2992 transport tripwire set, with the reason (#9410)
6+
7+
`packages/runtime/src/dispatcher-plugin.ts` has both properties that make a file
8+
a plausible landing site for a realtime transport, and neither the conformance
9+
test nor the protocol page said anything about it. It **mounts routes**
10+
`/actions`, `/automation` and `/packages`, the registration path separate from
11+
the `@objectstack/rest` one — and it **already writes SSE**: two
12+
`text/event-stream` sites with `no-cache` and `keep-alive`, working plumbing an
13+
agent could extend without writing any new transport mechanics. None of the five
14+
`#2992` / ADR-0096 D4 transport tripwires watch it, so a subscribe/fan-out
15+
transport wired there mints no `TRANSPORT-WIRED` key, produces no UNCLASSIFIED
16+
surface and reds no build. The protocol page stated the general limitation ("a
17+
transport wired outside the watched files produces no key and no failure")
18+
without naming the specific already-SSE-capable file sitting inside it.
19+
20+
**This change is a recording. It changes no behaviour**: no probe is added, no
21+
key is minted, and no matrix row is written for the two existing sites.
22+
23+
The reason is recorded because it is the part a reader cannot re-derive cheaply.
24+
Those two `text/event-stream` sites are **per-request AI response streaming, not
25+
realtime subscription fan-out**: each drains one `AsyncIterable` that the route
26+
handler itself returned into that same request's response body and then calls
27+
`res.end()` — the second site's own source comment names its producer as the AI
28+
routes. No subscriber is registered, no event is delivered to a *set* of
29+
recipients, and the file carries no upgrade handler, no subscribe registration
30+
and no realtime-service call. Watching it with the existing mechanics pattern
31+
would therefore mint a key on day one for a surface that is not the hazard
32+
`#2992` is about, leaving only two exits: classify two non-realtime sites in the
33+
matrix vocabulary, or weaken the pattern. Neither is acceptable, so the file
34+
stays out and the boundary is written down instead.
35+
36+
It is written in the two places a reader actually lands. In
37+
`authz-conformance.test.ts` the note closes the tripwire probe list, so a reader
38+
who has just finished enumerating the watched set reads the set's boundary in
39+
the same breath — beside, and explicitly distinguished from, the pre-existing
40+
`#5519` mention of the same file, which is about anonymous gates on the mounted
41+
routes and is a different point. In `realtime-protocol.mdx` it extends the
42+
identity-admission callout at the exact sentence that states the general
43+
limitation.
44+
45+
The exclusion is drawn on **fan-out, not on the SSE content type**, and both
46+
records say so: wiring an upgrade handler, a subscribe registration or a
47+
realtime-service call into that file puts it back inside the hazard while the
48+
recorded boundary still claims otherwise. Promoting it into the tripwire
49+
population with such a fan-out-specific marker is written into #8347's
50+
acceptance as a precondition of the WebSocket/SSE transport landing, so the
51+
design effort is spent when the hazard becomes real rather than now.

content/docs/protocol/kernel/realtime-protocol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The **Real-Time Protocol** describes how live data synchronization is intended t
1212
<Callout type="warn">
1313
**Implementation status (v1):** The shipping realtime service is an **in-memory pub/sub adapter** (`@objectstack/service-realtime`, `InMemoryRealtimeAdapter`) plus a **long-polling** client (`RealtimeAPI` in `@objectstack/client`). The `IRealtimeService` contract reserves an optional `handleUpgrade()` for a WebSocket handshake, but **no WebSocket (`/ws`) or SSE (`/api/v1/stream`) transport is wired up yet** — those sections below document the planned wire protocol, not a deployed endpoint. The in-memory adapter is **single-instance only** (v1 deployment contract); a Redis-backed adapter for multi-node HA is a post-GA fast-follow. Treat the WebSocket/SSE message formats, connection limits, and debug endpoints in this page as a forward-looking design spec until that transport lands.
1414

15-
**Identity admission (framework#2992, ADR-0096 D4):** today's delivery path is a trusted server-internal fan-out with **no per-recipient authorization** — subscriptions carry no principal and events carry the full record body. Before any client transport ships, delivery must re-check each subscriber's authority (RLS/FLS/tenant) per event — the subscribe-time permission check shown below is *not* sufficient — or switch to id-only payloads with client re-fetch. CI backs this with a **classification** ratchet rather than a proof of it: the `realtime-delivery-authz` row is `experimental` — it records the posture above rather than an enforcement site — and the transport tripwires watch a **curated list of realtime source files**, so wiring a WebSocket/SSE/subscribe transport in one of them turns it into an **unclassified surface** and reddens the build until the row is upgraded with its enforcement site. That puts the admission requirement in front of a reviewer; it does not check that the re-check was written — upgrading the row is a hand edit, and the build goes green on that edit alone. A transport wired outside the watched files produces no key and no failure.
15+
**Identity admission (framework#2992, ADR-0096 D4):** today's delivery path is a trusted server-internal fan-out with **no per-recipient authorization** — subscriptions carry no principal and events carry the full record body. Before any client transport ships, delivery must re-check each subscriber's authority (RLS/FLS/tenant) per event — the subscribe-time permission check shown below is *not* sufficient — or switch to id-only payloads with client re-fetch. CI backs this with a **classification** ratchet rather than a proof of it: the `realtime-delivery-authz` row is `experimental` — it records the posture above rather than an enforcement site — and the transport tripwires watch a **curated list of realtime source files**, so wiring a WebSocket/SSE/subscribe transport in one of them turns it into an **unclassified surface** and reddens the build until the row is upgraded with its enforcement site. That puts the admission requirement in front of a reviewer; it does not check that the re-check was written — upgrading the row is a hand edit, and the build goes green on that edit alone. A transport wired outside the watched files produces no key and no failure. One file is on record as deliberately outside that set: `packages/runtime/src/dispatcher-plugin.ts` both mounts routes and already writes `text/event-stream`, but its two SSE sites are **per-request AI response streaming** — each drains one iterable the route handler returned into that same request's response body — and not **subscription fan-out**, so watching them would mint a transport key for a surface this admission requirement is not about. That exclusion is drawn on fan-out, not on the SSE content type: wiring an upgrade handler, a subscribe registration or a realtime-service call into that file puts it back inside the hazard, and promoting it into the watched set with such a fan-out-specific marker is a precondition on the WebSocket/SSE transport work rather than something the current tripwires would catch.
1616
</Callout>
1717

1818
## Why Real-Time Matters

packages/qa/dogfood/test/authz-conformance.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,40 @@ const PROBES: ReadonlyArray<{ file: string; re: RegExp; key: (m: RegExpExecArray
225225
key: () => tripwireKey('realtime:rest-server.ts:route'),
226226
},
227227

228+
// ── #9410 — the tripwire population's DELIBERATE boundary ─────────────
229+
//
230+
// `packages/runtime/src/dispatcher-plugin.ts` is NOT in the list above, and
231+
// that is a decision on record rather than an oversight — read this before
232+
// concluding it was simply missed. It has both properties that make a file a
233+
// plausible landing site for a realtime transport: it mounts routes
234+
// (`/actions`, `/automation`, `/packages` — the separate registration path
235+
// named in the #5519 note above, which is a DIFFERENT point about anonymous
236+
// gates, not this one), and it already writes SSE — content type,
237+
// `no-cache`, `keep-alive` and all.
238+
//
239+
// Its two `text/event-stream` sites are nevertheless outside the #2992
240+
// tripwire set, because they are per-request AI response streaming, not
241+
// realtime subscription fan-out: each drains one `AsyncIterable` that the
242+
// route handler itself returned into that same request's response body, then
243+
// calls `res.end()`. No subscriber is registered, no event is delivered to a
244+
// SET of recipients, and the file carries no upgrade handler, no subscribe
245+
// registration and no realtime-service call. Watching them with the mechanics
246+
// pattern would mint a TRANSPORT-WIRED key on day one for a surface that is
247+
// not the hazard #2992 is about, and the only exits from that red would be to
248+
// classify two non-realtime sites in the matrix vocabulary or to weaken the
249+
// pattern — so this record adds no probe, no key and no matrix row.
250+
//
251+
// ⚠️ The boundary is drawn on FAN-OUT, not on the SSE content type. Wire an
252+
// actual subscription transport into that file — an upgrade handler, a
253+
// subscribe registration, a realtime-service call — and it is back inside the
254+
// hazard while this paragraph still says otherwise: nothing here will fail
255+
// for you. Promoting the file into the population with a fan-out-specific
256+
// marker (NOT the bare content type, which is precisely what would over-match
257+
// the two sites above) is written into #8347's acceptance as a precondition
258+
// of the WS/SSE transport landing. The twin record, for a reader arriving
259+
// from the docs side, is the identity-admission callout in
260+
// `content/docs/protocol/kernel/realtime-protocol.mdx`.
261+
228262
// ── ADR-0096 / #3167 — MCP execution-surface identity pins ─────────────
229263
// (1) The HTTP `/mcp` handler must stay classified (a new sibling MCP data
230264
// handler → UNCLASSIFIED). (2) Its caller-identity threading: handleMcp must

0 commit comments

Comments
 (0)