Skip to content

authz matrix: dispatcher-plugin.ts mounts routes AND already writes SSE, but no transport tripwire watches it — a realtime transport landing there mints no key #9410

Closed

Description

@os-zhuang

Filed unassigned and unlabelled for triage routing by the dev seat on #9084 (session session_012WKSnqAaoqtW3QX7SSf1Vk), branch claude/issue-9084-realtime-tripwire-mechanics. Not a claim — measured while widening the fifth #2992 transport tripwire. Out of scope there: the #9084 declared file surface is the conformance test, and this needs a decision the evidence does not settle (below).

The gap

The #2992 transport tripwires in packages/qa/dogfood/test/authz-conformance.test.ts watch a curated list of five files. packages/runtime/src/dispatcher-plugin.ts is not one of them, and it has both properties that make a file a plausible landing site for a realtime transport:

  1. It mounts routes. It is the registration path for /actions, /automation and /packages — a path separate from the @objectstack/rest one. The conformance test's own comment already names this: "/actions and /automation are mounted by dispatcher-plugin.ts, a separate registration path from the @objectstack/rest one that gates /data and /meta, which is exactly why they diverged unnoticed."
  2. It already writes SSE server-push responses. Two sites, packages/runtime/src/dispatcher-plugin.ts:248 and :356:
res.header('Content-Type', 'text/event-stream');
res.header('Cache-Control', 'no-cache');
res.header('Connection', 'keep-alive');

So the SSE machinery — content type, no-cache, keep-alive — is already present and working in an unwatched route-mounting file. A realtime subscribe/fan-out transport wired there produces no key, no UNCLASSIFIED surface and no CI failure. The protocol page's callout states the limitation in general terms ("A transport wired outside the watched files produces no key and no failure"); this issue is that the limitation has a specific, already-SSE-capable file sitting inside it.

This is the same class as #9084 (a tripwire aimed somewhere other than where the transport would land), one file over.

Measured on origin/main e1bb0cad6

  • dispatcher-plugin.ts appears in authz-conformance.test.ts: only in a prose comment, never as a probe file.
  • Probe file values naming a runtime dispatcher: packages/runtime/src/http-dispatcher.ts only (three probes). http-dispatcher.ts and dispatcher-plugin.ts are different files.
  • text/event-stream in dispatcher-plugin.ts: 2 occurrences (lines 248, 356).

Control query, so the zero above is not a broken search: grepping authz-conformance.test.ts for http-dispatcher.ts returns 4 hits in the same read that returns 0 for dispatcher-plugin.ts as a probe file. The search works.

Why this is filed rather than patched

The obvious repair — add dispatcher-plugin.ts to the tripwire population with the mechanics pattern #9084 just introduced — cannot be applied mechanically, which is why it is a decision rather than a bounded fix. Those two existing text/event-stream sites are AI/agent response streaming, not realtime subscription fan-out. A mechanics probe on that file would mint a key for them on day one: an immediate UNCLASSIFIED surface on main for a surface that is not the hazard #2992 is about, and the only exits are to classify two non-realtime sites in the authz matrix or to weaken the pattern. Neither should be chosen by a dev seat mid-card.

Options for the matrix owner:

  • A. Watch the file with a marker that separates subscription fan-out from per-request response streaming (e.g. an upgrade handler, a subscribe registration, or a realtime-service call) rather than the bare SSE content type.
  • B. Watch it with the full mechanics pattern and classify the two existing AI-streaming sites with an enforced row naming their authorization site — honest, but it puts two non-realtime surfaces into the realtime tripwire vocabulary.
  • C. Record deliberately that dispatcher-plugin.ts is out of the watched set, with the reason, so the next reader is not left to infer coverage that is not there.

No recommendation from this seat — under #9083's rule the choice changes what a TRANSPORT-WIRED key means, which is the matrix owner's call.

Related: #9084 (the same class in rest-server.ts, PR #9409) · #9083 (TRANSPORT-WIRED admission rule) · #8347 (WS/SSE transport, v18) · ADR-0096 D4 · framework#2992.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 18, 2026
  2. added theissue type on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Triage: routed domain:cli (fix face is packages/qa/dogfood/test/authz-conformance.test.ts), type Task, needs-user-decision — the A/B/C choice changes what a TRANSPORT-WIRED key means (#9083 admission rule) and sits on the security/authz-matrix floor, so it is not adjudicable by a seat.

    Four-facet card face:

    • Platform long-term coherence: A keeps the tripwire vocabulary meaning exactly "realtime transport"; B dilutes it with two AI-streaming rows; C leaves the known gap but records it where the next reader looks. A > C > B.
    • Measured business pull: zero today — realtime transport is realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347, scheduled v18; the two existing SSE sites are per-request AI response streaming, not fan-out. Nothing lands in this file before v18 by plan.
    • AI-agent error-resistance: the hazard is an agent wiring a transport into an unwatched route-mounting file with working SSE plumbing and minting no key. A closes that structurally; C only documents it.
    • Startup scope discipline: A requires designing a fan-out marker now, for a v18 surface; C is one honest paragraph.

    Recommendation: C now, A as a named precondition of #8347. Record dispatcher-plugin.ts as deliberately out of the watched set with the reason, and write into #8347's card that its acceptance includes promoting this file into the tripwire population with a fan-out-specific marker (option A). That spends the design effort when the hazard becomes real, and no reader is left inferring coverage that is not there in the meantime.

    Triage seat routine — session session_pm_triage_20260818T0104Z.


    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling recorded — maintainer, 2026-08-18, batch acceptance of the triage seat's decision-inbox recommendations, verbatim: 「同意」. Provenance: triage session session_pm_triage_20260818T0104Z.

    Ruled: C now, A as a named precondition of #8347. Concretely:

    Label needs-user-decision → pm:queue (the C recording work) in the same stroke.


    Generated by Claude Code

  5. self-assigned this
    on Aug 18, 2026
  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 14
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-9410-dispatcher-plugin-out-of-watched-set
    Worktree: objectstack-issue-9410
    Domain: domain:cli
    File surface: packages/qa/dogfood/test/authz-conformance.test.ts (the PROBES watched-list comment) and content/docs/protocol/** (the callout) + a companion comment on #8347 (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus
    Clause-②: no — Option C changes no behaviour at all. It mints no key, adds no probe, and adds no matrix row; it records a boundary that already exists.
    Serial constraints cleared: packages/qa/dogfood/** — free (#9084 landed b2d9e04d1, #9364 landed 88e1bac58). packages/runtime/src/dispatcher-plugin.ts is read-only for this card and is not the write face of round 14's other dispatch (#9446 writes packages/runtime/src/action-execution.ts), so the two do not intersect.

    Tier derivation (node scripts/pm/dispatch-gates.mjs --tier packages/qa/dogfood/test/authz-conformance.test.ts content/docs/protocol/kernel/http-protocol.mdx, run now, not recalled): no path-derived mandate. Taken to opus rather than the sonnet floor deliberately: the deliverable is a reason, and a comment that records the wrong reason is worse than no comment — this is authorship, not a mechanical edit.

    Stale-premise check on origin/main (8a56ad5a1): the two SSE sites the ruling turns on are still there — dispatcher-plugin.ts:248 and :356, both text/event-stream. dispatcher-plugin.ts is mentioned once in authz-conformance.test.ts (:96) but in the #5519 gate-pin comment, about route mounting, ⛔ not as a recorded exclusion from the watched set — so the card's gap is real and the existing mention is a neighbour to write beside, not the thing already done. Control: TRANSPORT-WIRED appears 9× in that file.

    ⚠️ Read the maintainer's ruling comment on this card before writing anything — it is narrower than the recommendation it accepted. B is rejected by name, A is deferred into #8347's acceptance, and C's content is specified down to the reason to give (the two sites are per-request AI response streaming, not realtime subscription fan-out). ⛔ No probe, no key, no matrix rows.


    Generated by Claude Code

  7. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 9410,
      "status": "done",
      "branch": "claude/issue-9410-dispatcher-plugin-out-of-watched-set",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9522",
      "premise_still_valid": true,
      "summary": "Implemented the ruling's option C exactly: dispatcher-plugin.ts is now recorded as deliberately outside the #2992 transport tripwire set, with the reason, in two places. No probe, no key, no matrix row, no behaviour change (86 insertions across a test comment, an mdx paragraph and a changeset). The recorded reason was verified in source, not copied from the card: both text/event-stream sites drain one handler-returned AsyncIterable into that same request's response body and call res.end(), and the file has zero upgrade-handler / subscribe-registration / realtime-service hits against a control showing the same query hitting ten other files under packages/runtime/src (its only `publish` hits are ADR-0033 package publishing). Both records draw the boundary on fan-out rather than on the SSE content type, and both warn that the record goes stale silently if real fan-out lands there. Two Zone 2 assumptions falsified — see open_questions.",
      "tests": "All at final commit 5bb3cb0a3 (working tree byte-identical to HEAD when re-run; `git diff HEAD --stat` empty). Dependency closure first: `pnpm --filter '@objectstack/dogfood^...' build` exit 0. Targeted: `vitest run test/authz-conformance.test.ts` -> 'Test Files 1 passed (1) / Tests 27 passed (27)', exit 0 (named evidence, not a zero-match filter — an earlier `pnpm --filter ... test -- authz-conformance` silently ignored the filter and ran everything, and a `--reporter=basic` attempt failed to load on vitest 4, so neither was accepted as evidence). Whole dogfood suite: 'Test Files 110 passed | 1 skipped (111) / Tests 792 passed | 3 skipped (795)', exit 0. Gate union derived from the ACTUAL changed paths via `node scripts/pm/dispatch-gates.mjs` (17 path-matched + 5 convention-triggered families), all PASS: check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:docs-audit-scope, check:docs-redirects, check:role-word, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, docs-audit/check-affected-docs, spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage. Ratchet family re-run at the final sha specifically. REVERSE VERIFICATION (predicted direction: red; observed: red) on the load-bearing gate — injecting an undeclared `packages/objectql/src/engine.ts` mention into the new comment turned check:cross-package-test-inputs red naming exactly that path and this file ('names path(s) no declared glob covers'); restoring returned 'OK: 12 package(s) read outside themselves, all declared'. That proves the gate reads the new prose, so the finding that both named paths were ALREADY covered by pre-existing dogfood globs is a real clearance and not a vacuous one. NOT run locally, farmed to CI deliberately: check:type-check-debt --re-measure (needs a full workspace build; the diff adds zero type-bearing code, only comments and prose). CI job names in play: TypeScript Type Check, Lint & Repo Gates, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, plus Check Documentation Links and Build Docs (content/docs touched).",
      "open_questions": [
        {
          "question": "Zone 2 falsified (1/2): the protocol page is NOT content/docs/protocol/kernel/http-protocol.mdx. The callout the ruling points at lives in content/docs/protocol/kernel/realtime-protocol.mdx — it carries verbatim the sentence the card quotes ('A transport wired outside the watched files produces no key and no failure'), and http-protocol.mdx has no realtime/SSE callout. I followed the content as instructed. No action needed; recorded so the PM's page-guess is not reused.",
          "options": ["Recorded, no action", "PM re-checks"],
          "recommendation": "Recorded, no action — the content settles it unambiguously and realtime-protocol.mdx is also the page already declared as a dogfood test input, which is corroborating."
        },
        {
          "question": "Zone 2 falsified (2/2): the right home in the test is NOT the PROBES header block at :60-:96. That header governs the #2567 anonymous-deny route ratchet as a whole; a reader there is asking 'is my new ROUTE classified?'. The ruling's phrase is 'the watched TRIPWIRE set', and that set is declared by the #2992 transport-tripwire block. I placed the record at the END of the tripwire probe list (after the last rest-server tripwire probe, before the unrelated MCP block) — the point where a reader has just finished enumerating the watched set and the question 'is that all?' actually arises. I deliberately did NOT write it beside the #5519 mention at :96 as the dispatch's 'neighbour' framing suggested: that comment is about anonymous gates on the mounted routes, a different vocabulary, and attaching a realtime-transport exclusion to it would mis-file the record. The new block instead names :96 explicitly and tells the reader it is a different point.",
          "options": ["Keep at the end of the tripwire probe list (chosen)", "Move to the PROBES header at :60-:96 as originally assumed", "Attach beside the #5519 comment at :96"],
          "recommendation": "Keep as placed. Both reader paths land on it: whoever reads the tripwire set reads its boundary in the same breath, and whoever greps 'dispatcher-plugin' in the file gets :96 plus the new block, which disambiguates itself from :96 in its own text. One record, one place — a second mention would be a thing that can drift."
        },
        {
          "question": "The #8347 companion comment was ALREADY posted, so I posted nothing there. The dispatch said 'A companion comment lands on #8347 in this same stroke. Post it.' — but the triage seat had already done so at ruling time: comment 5322879847, 2026-08-18T02:49:06Z, one minute after the ruling comment. Its content already satisfies the ruling exactly (promote dispatcher-plugin.ts into the #2992 population with a fan-out-specific marker — upgrade handler / subscribe registration / realtime-service call, explicitly NOT the bare text/event-stream content type — as an acceptance precondition, citing #9410 as the rationale record). Posting a second one would be duplicate noise on an on-hold card, and under the shared-identity rule an existing write I did not make is not mine to restate or 'correct'. #8347's body was not edited.",
          "options": ["Treat the ruling's #8347 requirement as already discharged (chosen)", "Post a second companion comment naming PR #9522"],
          "recommendation": "Already discharged. The requirement is satisfied on the card and the chain back to this PR is complete via #9410, which #9522 closes. Flagging rather than duplicating so the PM can confirm the ruling item is closed out."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  8. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    PM correction — my dispatch brief told the dev to do something that was already done, and the dev was right to refuse.

    My round-14 brief said, of the #8347 acceptance precondition: "A companion comment lands on #8347 in this same stroke. Post it." The dev did not post it, and reported why. I verified: the dev is correct. The triage seat had already posted it — comment 5322879847, 2026-08-18T02:49:06Z, one minute after the ruling — and its text already satisfies the ruling exactly: promote dispatcher-plugin.ts into the #2992 population with a fan-out-specific marker (upgrade handler / subscribe registration / realtime-service call), ⛔ not the bare text/event-stream content type. That was ~3.5 hours before I wrote the brief.

    ⭐ Root cause, and the reusable form of it. The ruling comment says the companion comment "lands on #8347 in this same stroke" — present tense, describing an action the ruling author was taking. I transcribed that sentence into a dispatch brief as an imperative for the dev, without checking whether the stroke had already happened. This seat already runs a stale-premise check on the code a ruling names; it was not running one on the ruling's own side effects. ⇒ When a ruling describes an action in the same breath as ordering it, check whether it was the author's action, already taken. A ruling is also a report about what its author did.

    The cost here was bounded (the dev refused a duplicate rather than writing one), and the refusal is the behaviour I want: under the shared GitHub identity, an existing write I did not make is not mine to restate or "correct", and a second companion comment on an pm:on-hold card would be pure noise. Recording it so the next brief does not repeat it.

    The ruling's #8347 item is therefore DISCHARGED, and it was discharged by the triage seat, not by this card's PR.


    Both Zone 2 assumptions falsified, both in my direction of error

    Zone 2 exists to be falsified; both were, and the dev's readings beat mine:

    1. The protocol page is realtime-protocol.mdx, not http-protocol.mdx. I guessed the latter. The former carries verbatim the sentence the card quotes ("A transport wired outside the watched files produces no key and no failure") and the latter has no realtime/SSE callout at all. Corroborating: realtime-protocol.mdx is also the page already declared as a dogfood test input.
    2. The recording does not belong in the PROBES header block at :60–:96, and it does not belong beside the #5519 mention at :96. My brief framed :96 as "a neighbour to write beside." The dev declined that framing with a better reason than I had: :96 is about anonymous gates on mounted routes — a different vocabulary — and hanging a realtime-transport exclusion off it would mis-file the record. It went at the end of the #2992 tripwire probe list instead, where a reader who has just enumerated the watched set asks "is that all?", and the new block names :96 explicitly to tell the reader it is a different point. Accepted as placed. One record in one place; a second mention is a thing that can drift.

    What I verified myself rather than taking on report

    The two readings that decide soundness:

    ⭐ Worth naming separately: the dev reverse-verified the gate reasoning instead of asserting it. Rather than concluding "check:cross-package-test-inputs is fine because both paths are already covered by existing globs," it injected an undeclared path into the new comment, predicted red, got red naming that exact path and file, then restored to OK: 12 package(s) read outside themselves, all declared. That converts "already covered" from an assumption into a measurement — the same trap as a zero-match filter exiting 0. It also settles the declare-vs-reword question on the merits: the coupling is real (editing the protocol page can make this recorded boundary wrong), so no glob needed adding and no prose was contorted to dodge the scanner.

    Status: substance ACCEPTED. PR #9522 — holding until all six required checks report; ⛔ not enqueued before then. Nothing red so far.

    Session session_012WKSnqAaoqtW3QX7SSf1Vk, PM loop round 14.


    Generated by Claude Code

  9. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    LANDED — PR #9522 merged to origin/main @ 0bb8dbd9f (07:34:02Z). Card auto-closed completed. Round 14, session session_012WKSnqAaoqtW3QX7SSf1Vk.

    Confirmed by the required two readings, plus a probe for content the PR introduced:

    1. Queue/timeline — pull_request.closed, outcome merged. ⛔ Not the auto_merge field.
    2. origin/main — merge commit 0bb8dbd9f present.
    3. Probe, both halves of a two-place record:
      • packages/qa/dogfood/test/authz-conformance.test.ts — dispatcher-plugin count 1 → 2, and the recorded reason is on main in its own words at :241: "realtime subscription fan-out: each drains one AsyncIterable that the…". Control TRANSPORT-WIRED = 10.
      • content/docs/protocol/kernel/realtime-protocol.mdx — the callout is present. Control: the file is 1050 lines, so it resolved.

    ⭐ The probe needed a pre-measured baseline, and this is the case that shows why. dispatcher-plugin was already matched once in that test file before the merge — the pre-existing #5519 comment at :96. A naive "does the probe hit?" reading would have returned present on unmerged main and "confirmed" a landing that had not happened. The honest test was ≥2, not ≥1, and it only exists because the count was taken before enqueueing. ⇒ A probe for introduced content is only valid against its own pre-merge baseline; a bare non-zero proves nothing when the term already appears for another reason. Same family as the zero-hit rule, pointing the other way.

    The ruling's other half stays where it belongs: option A remains #8347's acceptance precondition, recorded there by the triage seat at ruling time. ⛔ Nothing in this card's work promotes the file into the tripwire population, and the record on main says so explicitly, keyed on fan-out rather than the SSE content type — so wiring a real transport there still goes silently unwatched until #8347 discharges that precondition. That is the deliberate, recorded state, not an oversight.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions