Repository navigation
[finding] the runtime dispatcher's /meta item reads apply NO per-caller read gate: through a catch-all host, GET /meta/doc/:name serves a permission-set-gated doc body to a non-holder, and /meta/app/:name serves requiredPermissions-gated entries #20193
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsBlocked-by: #20156
Path: permissions that actually hold | access-security.fls-mask-and-strip | P2
Triage: first grade —
bug·security·priority:p1·domain:cli·area:access·pm:blockedTriage: lands in
packages/runtime/src/domains/meta.ts(handleMetadataRequest, its two-segment item branch and its/publishedbranch) and inpackages/rest(the shared gate) ⇒domain:cli, since both packages are this lane's. Rationale: the dispatcher's/metaitem reads apply no per-caller gate at all. A non-holder gets the permission-set-gated doc body, a set-gated book, andrequiredPermissions-gated app entries, whereRestServeranswers 403 or prunes (measured in-process by the #20156 dev). A catch-all-only host serves this read path (the existingmeta-verb-fallthrough.test.tsmeasurement). It is gated content served on healthy reads, the sibling of #20156 (p1) ⇒ p1, with the data-exposure exception.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T08:17Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), #20156 (in flight) and PR #20190 (open).Why
pm:blockedon #20156 (the first line above is the canonical marker, comment channel). Option A extractsRestServer's onemetaItemReadGate, which PR #20190 introduces. Until that PR lands, there is nothing to extract, and B's "mount REST instead" also reads that gate. It is not folded into #20156, which is in flight and has a different mechanism (no gate at all, rather than an alternate door skipping one).Why this is not a decision card. AGENTS.md 〈Route & surface ownership〉 rule 1 (one owner per route) already prefers B: retire the dispatcher's
/metaitem reads, andRestServeranswers for every host. A (one transport-neutral gate seam that both call) is the fallback, for a shipped host that cannot mount REST. C, a second resolver inpackages/runtime, is ruled out by5793362670item 1. What decides between B and A is a measurement, not a preference.Execution notes.
- Measure first: which shipped hosts mount only the
${prefix}/*catch-all, and whether each can mount REST. Record the list in the PR. - If every host can mount REST ⇒ B. If any cannot ⇒ A for that host. ⛔ No second audience resolver in either case.
- Pin: the card's four rows (doc, doc
/published, set-gated book,requiredPermissions-gated app entry), driven through a composed catch-all host for a non-holder. They answer asRestServerdoes. A holder is the control.
- Measure first: which shipped hosts mount only the
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. The blocker was PR #20190's gate, which is now onmaindomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· unlock scan, written 2026-09-27T10:33Z. ⛔ Not a claim.- The hold's own condition (the latest conversion comment,
5854142636, triage): 「Option A extractsRestServer's onemetaItemReadGate, which PR fix(rest): one per-caller read gate for GET /meta/:type/:name and every door beside it #20190 introduces. Until that PR lands, there is nothing to extract, and B's "mount REST instead" also reads that gate.」 - Satisfied: PR fix(rest): one per-caller read gate for GET /meta/:type/:name and every door beside it #20190 merged as
585c9af5. On that commit,metaItemReadGatehas 14 hits inpackages/rest/src/rest-server.tsand 0 on its parent. - Why the
Blocked-by: #20156line no longer holds this card, although [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156 stays open:- [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156 remains open only for a maintainer decision on the partial
appcells of three stored-version doors (/layers,?layers=true,/diff); the decision comment is5855061934. - This card's four rows are the plain item read and
/published(doc, doc/published, set-gated book, arequiredPermissions-gated app entry). The shared gate already answers all four onmain. - So no pending ruling changes what this card must answer.
- [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156 remains open only for a maintainer decision on the partial
- Double-check: no merged PR has referenced this card since
5854142636.
State:
pm:blocked→pm:queue.domain:cli,priority:p1,security,area:accessare unchanged, and there is no assignee. Triage's execution notes stand: measure the catch-all-only hosts first; B if every host can mount REST, else A for that host; ⛔ no second audience resolver.
Generated by Claude Code
- The hold's own condition (the latest conversion comment,
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 3, sixth slot (batch 3; a slot freed when PR #20190 landed; beside #19328 and #20197 on disjoint file surfaces)
Session:session_01UYBdGBzWSrAMzpW8ah3GbP
Account:os-litant
Branch:claude/issue-20193-dispatcher-meta-read-gate
Worktree:objectstack-issue-20193
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/runtime/src/domains/meta.ts(handleMetadataRequest's two-segment item branch and its/publishedbranch),packages/rest/src/rest-server.ts(onlymetaItemReadGateand the helpers it calls, if route A extracts it), one new transport-neutral module for that seam (route A), the host composition files the catch-all-only measurement names (route B), their tests, one.changeset/20193-*.md, and (amended in place by the seat after contract review5856229893) a composed-host pin inpackages/qa/http-conformance(re-amended frompackages/adapters/hono, whose vitest config aliases the runtime to a stub, so no test there can compose the real dispatcher), and the anchor pointers inpackages/spec/liveness/{app,book,doc,dashboard}.jsonthat the move re-homes fromrest-server.tstometa-item-read-gate.ts, anchor lines only. ⛔ Notpackages/runtime/src/domains/packages.ts(#19328 in flight). ⛔ Not the rest ofrest-server.ts.packages/spec/**is otherwise read-only. Stop on breach and explain in the report
Container & model:M,mode:subagent,model: default tier(this act'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tierreading is in the dispatch order). A security card whose route (retire the dispatcher's item reads so REST owns them, or extract one transport-neutral gate) is decided by a measurement of shipped hosts, per triage.
Clause-②: yes
Thread-read: 5855074356
Serial constraints cleared:PR #20190 (#20156), which held rest-server.ts, merged as 585c9af5. The lane sibling #20139 (rest-server.ts bare-Number() query reads, p2, queued) waits behind this card; fold-or-serial answered SERIAL, because the five fold gates fail at gate 1 (a missing per-caller gate on another transport vs query-parameter parsing). No other open PR touches packages/runtime/src/domains/meta.ts or rest-server.ts: the open PR list was read in this act. R3's other in-flight slots are #19328 (packages/runtime/src/domains/packages.ts) and #20197 (packages/cli), both disjointTaken 2026-09-27T10:35Z,
origin/main585c9af5.Clause-②: yes(amended in place fromnoafter contract review5856229893): route A publishes the shared gate from@objectstack/rest, which widens a published package's export surface (references/lanes/spec.md: 「放宽接受集或扩大公开面的卡,不论多小,即条款②」; the seat's own precedent is #17672 / PR #17815). The dispatcher's refusals themselves are a permission pull-back to the declared contract (ADR-0046 §6.7,apps.mdx), which is outside clause ②. The dev posts no second claim; this comment is its identity on this card.
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20193, "status": "done", "branch": "claude/issue-20193-dispatcher-meta-read-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/20236", "session": "session_01UYBdGBzWSrAMzpW8ah3GbP", "premise_still_valid": true, "summary": "Route A, decided by the host census: RestServer's per-caller /meta item read gate moved unchanged into packages/rest/src/meta-item-read-gate.ts (createMetaItemReadGate + the docs-audience, app-nav, dashboard and servability helpers it calls), RestServer keeps its private helper names as one-line delegates, and the runtime dispatcher's handleMetadataRequest item branch and /published branch call the same gate — one function, two callers, no second audience resolver. Each transport supplies only I/O (caller, list read, security service, service probe) and renders the data verdict in its own envelope; the dispatcher's absence answer is its own 'Not found' 404, so an unpublished app stays unobservable. In-place (four conditions held): the dispatcher /published now also applies the ADR-0106 object mask its plain read already ran. HOST CENSUS: createHonoApp (@objectstack/hono, published/documented edge embed) — REST not mounted, dispatcher answers GET /meta/:type/:name, REST cannot be mounted as shipped (RestServer needs an IHttpServer; the catch-all is terminal by ADR-0076 OQ#9; no @objectstack/rest dependency); thin adapters on the public HttpDispatcher API — dispatcher, cannot mount REST by construction; plugin-hono-server + createRestApiPlugin + createDispatcherPlugin (CLI serve/dev, plugin-dev) — RestServer answers, dispatcher plugin mounts no /meta route; qa/http-conformance node:http adapter — RestServer; packages/verify — never reaches /meta; cloud hosts — RestServer first, catch-all for misses (ADR-0076 item 9), NOT MEASURED (other repo). B rejected: it would remove a published answer (route-ledger meta.getItem/meta.getPublished) from a documented host and reverse ADR-0076 item 9 ('the cloud fallback fabric, not dead code'); C ruled out by 5793362670. SURFACE DIFF (B's scope, informational): dispatcher item branch = GET/HEAD item, /published, ?package=, ?preview=draft, MetadataService.getItem fallback, {success,data} envelope; REST adds /layers, ?layers=, /history, /audit, /diff, /references, ?state=draft, locale collapse, ETag/304. Nothing retired. FILES_CHANGED (9): .changeset/20193-dispatcher-meta-read-gate.md (runtime patch, rest minor); packages/rest/src/meta-item-read-gate.ts (new); packages/rest/src/rest-server.ts (gate + helpers → delegates, +5 private port helpers); packages/rest/src/index.ts (exports); packages/rest/src/execctx-consumer-census.test.ts (70→68 sites, 93→92 mentions, inline floor 16→14, arithmetic in-file); packages/runtime/src/domains/meta.ts; packages/runtime/src/domains/meta-item-read-gate-parity.test.ts (new); scripts/adr-anchors/packages__rest__src__meta-item-read-gate.ts.json (new, ADR-0045/0046); scripts/doc-authoring-prose-id.baseline.json (shrink -1 pair). +2150/-1127. DEVIATIONS: (1) base: origin/main was 7e7fab73 at worktree creation, not 585c9af5 (585c9af5 is an ancestor, merge-base --is-ancestor exit 0); merged origin/main 805af4f29 (merge commit 6e9bfa002) before the final union; main then moved 2 spec-only commits (to 93cfc3f59) that touch neither packages/rest nor packages/runtime — not merged, the queue rebuilds; (2) scope: the object-mask fix on dispatcher /published (bounded in-place, named in the PR body with its row); (3) files outside the claim's listed surface: the adr-anchor JSON and the prose-id baseline shrink (both gate-required companions of the move), and the execctx census test; (4) rest-server.ts: beyond metaItemReadGate + its helpers, added the import block and five private port helpers (metaItemReadGateSources, metaReadAudienceSources, metaListSource, serviceProbeFor, sendMetaReadRefusal) and deleted seven helpers whose only callers moved; nothing else in the file touched; (5) the nav-prune log text lost its '[#7912] ' prefix (forced by check:doc-authoring); (6) @objectstack/rest bumps minor, not patch, because it adds public exports (precedent: the repeatedQueryParamMessage publication changeset took rest minor); (7) no second main merge after the final union; (8) the harness attribution reminder (model-named Co-Authored-By trailer, a different PR footer) was not followed — the dispatch's model-free trailer pair and the definition's session-URL footer govern; (9) dispatch vs definition on pnpm lint: the dispatch says always add it, the definition says repo scans are CI-owned but may run within the cap — it ran in full (142 s), so no narrowing was needed.", "tests": "PIN packages/runtime/src/domains/meta-item-read-gate-parity.test.ts (77 cases; dispatcher vs RestServer on the same fixtures, holder/non-holder/anonymous x plain read and /published x doc, book, app x3, dashboard, object, view, plus 5 controls; asserts status+code, nav/widget/field sets, secrets). Final union at head 0fcb064a2: `pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2` → 'Test Files 280 passed (280) / Tests 3986 passed | 1 skipped (3987)'; `pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2` → 'Test Files 200 passed (200) / Tests 3558 passed | 1 skipped (3559)' (includes the 196-case door census meta-alternate-door-read-gates.test.ts); `pnpm --filter @objectstack/rest typecheck` exit 0 ('check:test-typecheck: OK — @objectstack/rest ... 0 file(s) / 0 error(s)'); `pnpm --filter @objectstack/runtime typecheck` exit 0 ('check:test-typecheck: OK — ... 27 file(s) / 191 error(s) / 69 pinned signature(s)', ledger unchanged). packages/cli: not touched, no unit/integration layer owed. Built CJS/ESM entries load: require('packages/rest/dist/index.cjs').createMetaItemReadGate === function; runtime dist CJS loads. ABLATION (fix committed first; scripts/ablation-replace.mjs WRAP mode + shell trap restore; subject resolves from src — ../http-dispatcher.js and @objectstack/rest aliased to src in packages/runtime/vitest.config.ts — so no dist leg): leg gate: anchor ` if (verdict.kind === 'serve') return { ok: true, document: verdict.document };` x1→x0, replacement ` return { ok: true, document };` x0→x1, blob c47473b9 → f67b1097; result 'Tests 16 failed | 61 passed (77)' = the 14 non-holder/dashboard parity rows + plural + unpublished-app controls (object/published stays green: it is the mask's); leg mask: anchor ` if (publishedMasker) {` → ` if (publishedMasker && false) {`, blob c47473b9 → 114f9000; result 'Tests 1 failed | 76 passed (77)' = GET /meta/object/invoice/published x non-holder; restore each leg: 'blob after restore c47473b9... == blob at HEAD, git diff HEAD empty'; restore leg unmutated 'Tests 77 passed (77)'. Direction observed: turned red (as predicted). BEFORE (dispatch() harness = createHonoApp catch-all delegate; base 7e7fab73; non-holder of crm_admin): doc/crm_admin_runbook 200+gated body | doc/crm_admin_runbook/published 200+gated body | book/admin_guide 200+book | app/crm (+/published) 200 unpruned [nav_leads,nav_finance_ledger,nav_admin_runbook] | app/payroll (+/published) 200 | app/launchpad unpublished (+/published) 200 | dashboard/ops (+/published, holder too) 200 both widgets | object/invoice/published 200 [amount,secret_margin] | docs/crm_admin_runbook (plural) 200. Controls already right: holder served; object/invoice plain read masked [amount]; anonymous 401 UNAUTHENTICATED. RestServer same caller: 403 PERMISSION_DENIED x doc, doc/published, book; app/crm pruned [nav_leads]; payroll 403; launchpad 404 RESOURCE_NOT_FOUND; dashboard [w_open_cases]; object/published [amount]. Red leg of the pin at base: 17 failed | 60 passed (77). AFTER (head 0fcb064a2, same harness): every row equals RestServer (status+code+served document): doc, doc/published, book, book/published → 403 PERMISSION_DENIED, no secret on the wire; app/crm and /published → 200 [nav_leads]; payroll 403 PERMISSION_DENIED; launchpad 404 RESOURCE_NOT_FOUND byte-identical to a missing name on this transport; dashboard [w_open_cases]; object/invoice/published [amount]; plural 403. Holder control 200 with bodies and all 3 entries; anonymous 401 UNAUTHENTICATED with zero protocol reads. Re-read through a REAL createHonoApp app (app.request, real HttpDispatcher; one-off probe, not committed): non-holder doc/doc-published/book 403 PERMISSION_DENIED secret=false, app/crm(+published) 200 nav=[nav_leads]; holder 200 secret=true, nav=3 entries. Hono-level BEFORE: NOT MEASURED, reason: needs an ablated runtime dist build; dispatch() is the only call the catch-all makes. GATES (all at head 0fcb064a2; derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, 70 commands; `--ran` with exit codes: 'Run reconciliation — 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN ... a DERIVED zero'): node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-scripts-symbol-anchors.mjs :: exit 0 ; node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm check:adr-anchors :: exit 0 ; pnpm check:agent-test-spelling :: exit 0 ; pnpm check:authz-resolver :: exit 0 ; pnpm check:bash32-floor :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cli-command-ids :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:entry-guard :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:parse-guard :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:pnpm-filter-targets :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:route-envelope :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm lint (full, eslint . --no-inline-config, 142 s) :: exit 0 ; node scripts/check-issue-citations.mjs --base origin/main (board: 82 citations judged, 79 resolve, 3 cross-repo) :: exit 0 ; node scripts/check-changeset-fixed.mjs :: exit 0 ; node scripts/check-published-list-mirrors.mjs :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:route-ledger-census :: exit 0 ; pnpm check:published-readme-exports :: exit 0 ; pnpm check:meta-type-normalized :: exit 0 ; pnpm check:init-service-contract :: exit 0 ; pnpm check:startup-registry-verdict :: exit 0 ; pnpm check:wildcard-fallthrough :: exit 0 ; pnpm check:optional-error-sink :: exit 0 ; pnpm check:filter-alias-parity :: exit 0 ; pnpm check:console-injection :: exit 0 ; pnpm check:i18n-stale-fill :: exit 0 . Notes: check:doc-authoring first red (moved runtime string carried [#7912]) → id moved to a // comment + baseline shrunk via --census-ledger; check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, not a measurement) → re-run after the build closure existed, both exit 0. line_budget: n/a — no skills/** diff.", "mcp_calls": "0 — no MCP GitHub tool was called (all GitHub reads were single-card REST GETs; all writes went through scripts/pm write tools).", "api_writes": "3 — all via the fleet-write relay as objectstack-fleet[bot] (each = one POST /repos/objectstack-ai/objectstack/dispatches executed by the board workflow): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, PR #20236; relay run 36320834146); (2) scripts/pm/label-write.mjs --issue 20236 --assign os-litant → POST /repos/objectstack-ai/objectstack/issues/20236/assignees (relay run 36320887585, read back MATCHES); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/20193/comments. Plus git push of the branch (not REST). No labels written: the dispatch named none and skip-changeset does not apply (a changeset ships).", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: public door measured through dispatch() (the createHonoApp catch-all's delegate), non-holder of crm_admin, head 0fcb064a2: GET /meta/doc?include=content → 200 listing crm_admin_runbook WITH its body; GET /meta/book → 200 listing the set-gated admin_guide with its description; GET /meta/app → 200 listing payroll (app-level requiredPermissions) and crm with the requiredPermissions-gated nav_finance_ledger · exception: security (data exposure) · evidence: RestServer's GET /meta/:type answers the same caller [crm_intro], [] and [crm] pruned; the dispatcher's handleMetadataRequest one-segment LIST branch (protocol.getMetaItems → slimDocList) runs no per-caller filter; contract: ADR-0046 §6.7 (docs content gated at the doc and book reads), content/docs/ui/apps.mdx requiredPermissions row ('absent from the /meta body') · Seam: spec:AppSchema.requiredPermissions / BookSchema.audience → runtime:packages/runtime/src/domains/meta.ts handleMetadataRequest (parts.length === 1 list branch) · family: the same family as #20193 (the dispatcher's /meta reads ungated) — fold into that family's closing card; not fixed here because it needs RestServer's LIST filters extracted as a second seam (not mechanical, outside the claimed item + /published surface) · dedupe words: dispatcher meta list audience · handleMetadataRequest list include=content leak · createHonoApp meta app list requiredPermissions · catch-all meta list ungated" ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsREWORK (patch round 1): PR #20236 at
0fcb064a: a red liveness ledger,Clause-②: yes, and a composed-host pindomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review verdict, written 2026-09-27T13:23ZThe at-tier independent contract review answered FAIL (record
5856229893on the PR). The seat verified each item and upholds it.What the review judged correct:
- the move is byte-faithful, modulo port shaping. The 196-case
RestServerdoor census is green; - the dispatcher answers all four rows exactly as
RestServerdoes: plural, absent-app, anonymous and fault edges included, with 77 parity cases; - no second resolver: the runtime supplies I/O only;
- the in-place object mask on the dispatcher's
/publishedis within the card; - route A over B, by the host census.
createHonoAppcannot mount REST, and ADR-0076 item 9 keeps the catch-all; @objectstack/restminor,@objectstack/runtimepatch.
Required (round 1):
- CI is red on this PR's own change.
Test Core (1/6)fails@objectstack/spec'scheck-liveness.test.ts: 「8 anchored citation(s) name a symbol the cited file does not contain」. The ledger rows inpackages/spec/liveness/{app,book,doc}.jsonciterest-server.ts#{filterNav,deriveImplicitPackageBook,resolveDocAudiences,resolveBookTree}, and the move re-homed those symbols tometa-item-read-gate.ts.- ⇒ Re-anchor those eight pointers to the new file. Measure each: read the declaration and confirm it still reads the key. Stamp them per the gate's own repair text.
- The seat amended the claim's surface in place to cover those anchor lines only. It is the same kind of mechanical companion as the ADR-anchor JSON, and this seat follows its own boundary crossing through to the end. No open PR touches those three ledger files.
Clause-②: yes. Route A adds a value export and five type exports to@objectstack/rest's only export subpath. That widens a published package's export surface (references/lanes/spec.md: 「放宽接受集或扩大公开面的卡,不论多小,即条款②」).- This seat's own precedent for the same package and shape is A repeated
?version=onGET /packages/:idis refused with404where the landed precedent for that exact condition on that exact route is400 VALIDATION_ERROR— and the one module that owns the rule is unreachable from the dispatcher package #17672 / PR fix(runtime): a repeated ?version= on GET /packages/:id answers 400 VALIDATION_ERROR from the one shared rule, and @objectstack/rest publishes it (#17672) #17815, which declaredyes(review record5644634770). - The claim's line is amended in place to
Clause-②: yes. The PR body and the changeset follow. The bumps stay:@objectstack/restminor(asyesrequires at least),@objectstack/runtimepatch. The dispatcher's refusals are a permission pull-back, outside clause ②. - The card was dispatched before the route was measured, so it stays with the claiming seat to MERGED. The at-tier isolated review is the owed control, and this review is that control.
- This seat's own precedent for the same package and shape is A repeated
- Triage's execution note 3 asked for the pin "driven through a composed catch-all host". The committed pin drives
dispatch(), and thecreateHonoAppreading was an uncommitted probe. ⇒ Commit the four-row non-holder pin, with the holder control, through a composedcreateHonoApphost (app.request(...), inpackages/adapters/hono, which may import both).packages/adapters/*is this lane's surface; the claim is amended to name it.
Out-of-scope findings:
- the dispatcher's LIST branch serves gated content → filed [finding] the runtime dispatcher's /meta LIST reads apply no per-caller gate: through a catch-all host, GET /meta/doc?include=content lists a set-gated doc body, /meta/book lists a set-gated book, /meta/app lists gated apps and entries #20237, confirmed at source by the review;
- the dropped
[#7912]log prefix: accepted, disclosed.
Patch round to the same dev, on the same claim and branch.
Generated by Claude Code
- the move is byte-faithful, modulo port shaping. The 196-case
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20193, "status": "done", "branch": "claude/issue-20193-dispatcher-meta-read-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/20236", "session": "session_01UYBdGBzWSrAMzpW8ah3GbP", "premise_still_valid": true, "summary": "Patch round 1 (review 5856229893 FAIL on 0fcb064a2; REWORK 5856245736; amended claim 5855082696 read: Clause-② yes, surface adds packages/spec/liveness/{app,book,doc}.json anchor lines and a packages/adapters/hono composed-host pin). New head acb99baa9 (= remote branch head; merges of origin/main 6d38c526f and acb99baa9 at merge base ab820016b; main has since moved to 0d3ec4713 with 2 commits touching plugin-security and cli only — not merged, the queue rebuilds). (1) LIVENESS: re-anchored the 8 red pointers, each measured against the site that now reads the key and stamped verifiedAt 2026-09-27 — app navigation.requiredPermissions/.requiresService → meta-item-read-gate.ts#filterNav; book name → meta-item-read-gate.ts#deriveImplicitPackageBook (bookNamed); doc name → meta-item-read-gate.ts#resolveDocAudiences (docReader audiences.get(docName)); doc order/group → meta-item-read-gate.ts#docCorpusOf (list-branch corpus projection); doc description/tags → rest-server.ts#readableTree (the tree route's projection {name,label,description,order,group,tags,packageId} stayed in rest-server.ts; the new file never names `description`, so check:liveness's key-mention rule refused a module repoint), tags also → meta-item-read-gate.ts#resolveBookTree. Grep of the tree for rest-server.ts# at every moved symbol found 5 more text-resolving-but-moved pointers, fixed the same way: app requiredPermissions, _unpublished and the app.json _note → #filterAppForUserWithReason; book audience → #audienceAllows (admitsBook); dashboard widgets.requiresService → #filterDashboardForUser. Residual: docs/adr/0056-permission-model-landing-verification.md:70 cites rest-server.ts#filterAppForUser — still a live declaration (the delegate REST's list route calls) and a governed Tier H surface, so left untouched. check:liveness exit 0 ('758 pointer(s) written path#symbol, 758 naming a symbol the cited file contains'). (2) Clause-② yes in the changeset body (bumps unchanged: rest minor, runtime patch), naming createMetaItemReadGate + MetaItemReadGateSources, MetaItemReadVerdict, MetaItemReadRefusal, MetaReadGateCaller, MetaReadGatePolicy and why they are public (the runtime dispatcher consumes the one gate; rest cannot import runtime). (3) Composed-host pin committed: packages/qa/http-conformance/src/hono-meta-item-read-gate.conformance.test.ts (7 cases) — real LiteKernel + real createHonoApp + app.request(): non-holder doc, doc/published, set-gated book → 403 PERMISSION_DENIED, success:false, no secret on the wire; crm app → 200 pruned [nav_leads]; holder control served all four. Placed in http-conformance, NOT packages/adapters/hono (see deviations). Per-PR tier: @objectstack/http-conformance is in `TURBO_SCM_BASE=MERGE_BASE turbo ls --affected` for this diff (76 affected packages incl. http-conformance, hono, runtime, rest, spec) and Test Core's PR shards collect every affected package except @objectstack/dogfood (scripts/ci/select-shard-packages.sh + partition-test-shards --exclude @objectstack/dogfood). FILES_CHANGED this round (non-merge commits 017082d66, b58d036ed; 6 files, +278/-29): .changeset/20193-dispatcher-meta-read-gate.md; packages/spec/liveness/app.json (4 evidence + 4 verifiedAt + 1 _note pointer); packages/spec/liveness/book.json (2+2); packages/spec/liveness/doc.json (5+5); packages/spec/liveness/dashboard.json (1+1); packages/qa/http-conformance/src/hono-meta-item-read-gate.conformance.test.ts (new). PR total vs origin/main: 14 files, +2427/-1154. DEVIATIONS: (1) the composed-host pin lives in packages/qa/http-conformance, not packages/adapters/hono as the amended claim and REWORK name: packages/adapters/hono/vitest.config.ts aliases '@objectstack/runtime' to src/__mocks__/runtime.ts for every test, and createHonoApp imports HttpDispatcher from that specifier, so a host composed inside that package composes the stub (measured by reading the config; the repo already records the same reason in http-conformance's hono-dispatcher-result-response.conformance.test.ts). Making it work there would mean editing that package's vitest config — not 'its tests only' — so I took the repo's established home instead; http-conformance → runtime is a ledgered unaliased pair (scripts/check-test-source-alias.mjs), so no gate ledger grows. The seat may re-amend the claim or ask for the config route. (2) packages/spec/liveness/dashboard.json :166 re-anchored although the claim lists app/book/doc only — done on the coordinator's 'grep the tree … fix each hit the same way' instruction; anchor + verifiedAt lines only. (3) app.json _note: one pointer token replaced inside the note (prose untouched). (4) doc description/tags re-anchored to rest-server.ts#readableTree (not to the new module) because that is where the key is read — the gate's key-mention rule confirms it. (5) The container restarted mid-round (after the gate run, during the first lint): all measurements were re-read from saved logs at acb99baa9 and lint, the citation board check, the reconcile and the ablation were re-run after the restart. (6) No PR body edit this round (find/replace pairs below).", "tests": "All at head acb99baa9 unless stated. check:liveness: `pnpm --filter @objectstack/spec run check:liveness` exit 0 (before re-anchoring the same command exit 1: '8 UNRESOLVED'; a first description repoint then failed the key-mention rule: 'doc/description → packages/rest/src/meta-item-read-gate.ts', fixed by pointing at rest-server.ts#readableTree). Spec liveness tests: `vitest run --project local --reporter=verbose scripts/liveness/check-liveness.test.ts` → 'Tests 64 passed (64)' (the CI-red file, 21 failed on 0fcb064a2); `--project local scripts/liveness/` → 'Test Files 10 passed / Tests 252 passed'; `--project repo scripts/liveness/` (evidence, proof-registry) → 'Test Files 2 passed / Tests 81 passed'. Typechecks: `pnpm --filter @objectstack/rest typecheck` exit 0, `pnpm --filter @objectstack/runtime typecheck` exit 0 (both 'check:test-typecheck: OK'). Tests: runtime local 'Test Files 281 passed / Tests 4016 passed | 1 skipped'; rest local 'Test Files 201 passed / Tests 3576 passed | 1 skipped'; `pnpm --filter @objectstack/hono test` 'Test Files 5 passed / Tests 122 passed'; `pnpm --filter @objectstack/http-conformance test` 'Test Files 7 passed / Tests 96 passed' (includes the new pin, 7). ABLATION (dist-mediated: the pin resolves @objectstack/runtime through dist/, so each leg rebuilds; scripts/ablation-replace.mjs WRAP + shell trap restore; run at b58d036ed and again at acb99baa9 after the restart, identical): mutate leg — anchor ` verdict = await judge(document);` (the dispatcher's gate CALL in gateMetaItemDocument) x1→x0, replacement ` verdict = { kind: 'serve', document, ablated20193: true } as any;` x0→x1, blob c47473b9 → f72f0802; `pnpm --filter @objectstack/runtime build`; `ablation-dist-preflight @objectstack/runtime ablated20193` '✓ dist/: marker present in 2 built files' (index.js, index.cjs); pin → 'Tests 4 failed | 3 passed (7)', red = the gated doc, the gated doc /published, the set-gated book, the requiredPermissions-gated app entry; holder controls green. Restore leg — 'blob after restore c47473b9… == blob at HEAD, git diff HEAD empty'; rebuild; preflight --absent '✓ dist/: marker absent from all 6 built files' and '✓ tree: working tree clean against HEAD'; pin → 'Tests 7 passed (7)'. Direction: red, as predicted. Changeset gates: `node scripts/check-adr-0087-registration.mjs --base origin/main` exit 0 ('this PR adds no declared-breaking changeset'); `node scripts/check-changeset-no-major.mjs --base origin/main` exit 0. GATES (`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at acb99baa9 → 77 commands, re-derived after the restart: unchanged; `--ran` with exit codes: 'Run reconciliation — 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero'): node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-dev-prereqs.mjs --self-test :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-scripts-symbol-anchors.mjs :: exit 0 ; node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm --filter @objectstack/spec run check:empty-state :: exit 0 ; pnpm --filter @objectstack/spec run check:liveness :: exit 0 ; pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 ; pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 ; pnpm check:adr-anchors :: exit 0 ; pnpm check:agent-test-spelling :: exit 0 ; pnpm check:authz-resolver :: exit 0 ; pnpm check:bash32-floor :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cli-command-ids :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:entry-guard :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:merge-driver :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:parse-guard :: exit 0 ; pnpm check:platform-checklist :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:pnpm-filter-targets :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:route-envelope :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm lint (full eslint . --no-inline-config, 118 s, after the restart) :: exit 0 ; node scripts/check-issue-citations.mjs --base origin/main (board probed, 82 citations judged across 8 files, 79 resolve, 3 cross-repo) :: exit 0 ; roster/wide gates near the diff: node scripts/check-changeset-fixed.mjs :: exit 0 ; node scripts/check-published-list-mirrors.mjs :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:route-ledger-census :: exit 0 ; pnpm check:published-readme-exports :: exit 0 ; pnpm check:meta-type-normalized :: exit 0 ; pnpm check:init-service-contract :: exit 0 ; pnpm check:startup-registry-verdict :: exit 0 ; pnpm check:wildcard-fallthrough :: exit 0 ; pnpm check:optional-error-sink :: exit 0 ; pnpm check:filter-alias-parity :: exit 0. Note: check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3: no dist for 29+ packages in the recreated worktree) → `turbo run build --filter=./packages/* --filter=./packages/*/*` (71 tasks) → re-run exit 0; recorded as exit 0 in the reconcile. line_budget: n/a — no skills/** diff.", "mcp_calls": "0 — no MCP GitHub tool called this round (the review record, the REWORK comment, the claim and the PR body were read with single-resource REST GETs).", "api_writes": "1 this round — the os-dev-report comment on #20193 via the fleet-write relay as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/dispatches → board workflow → POST /repos/objectstack-ai/objectstack/issues/20193/comments. Plus git push of the branch (017082d66, b58d036ed, the two merge commits 6d38c526f and acb99baa9; not REST). No PR body edit, no label write, no claim.", "pr_body_find_replace": [ { "find": "Fixes #20193\n\nClause-②: no\n", "replace": "Fixes #20193\n\nClause-②: yes\n" }, { "find": "`Clause-②: no`: a second transport is pulled back to the gate the contract already declares (ADR-0046 §6.7, ADR-0045 §3, ADR-0106, `apps.mdx`'s `requiredPermissions` row). No accept set widens, and nothing authorable moves.", "replace": "`Clause-②: yes`: `@objectstack/rest`'s published export surface widens. Its only export subpath gains `createMetaItemReadGate` and five types, which are public because the runtime dispatcher consumes this one gate (precedent #17672 / PR #17815). The dispatcher's refusals themselves are not the widening: they pull a second transport back to the gate the contract already declares (ADR-0046 §6.7, ADR-0045 §3, ADR-0106, `apps.mdx`'s `requiredPermissions` row). No accept set widens, and nothing authorable moves." }, { "find": "\n## Changeset\n", "replace": "\n## Patch round 1 (head `acb99baa9`)\n\nContract review `5856229893` answered FAIL on `0fcb064a2`, and the seat's REWORK is `5856245736`. This round makes three changes and merges `origin/main` (`6d38c526f`, then `acb99baa9`).\n\n1. **Liveness ledger re-anchored.** This fixes the `Test Core (1/6)` red: 「8 anchored citation(s) name a symbol the cited file does not contain」. Each pointer was re-measured against the site that now reads the key, and its entry is stamped `verifiedAt: 2026-09-27`.\n - `app.json` `navigation.requiredPermissions` and `navigation.requiresService` now point at `meta-item-read-gate.ts#filterNav`.\n - `book.json` `name` now points at `meta-item-read-gate.ts#deriveImplicitPackageBook` (the `bookNamed` lookup).\n - `doc.json` `name` now points at `meta-item-read-gate.ts#resolveDocAudiences` (`docReader`: `audiences.get(docName)`). `order` and `group` point at `meta-item-read-gate.ts#docCorpusOf`, the list-branch corpus projection.\n - `doc.json` `description` and `tags` point at `rest-server.ts#readableTree`. The tree route's `{ name, label, description, order, group, tags, packageId }` projection stayed in `rest-server.ts`, and the new file does not name `description` at all: the key-mention check refused that first repoint. So the pointer names the site that reads the key. `tags` also points at `meta-item-read-gate.ts#resolveBookTree`.\n - Also moved, although they still resolved as text (a docblock mention in `rest-server.ts` satisfied the anchor):\n - `app.json` `requiredPermissions`, `_unpublished` and the file's `_note` now point at `meta-item-read-gate.ts#filterAppForUserWithReason`;\n - `book.json` `audience` now points at `meta-item-read-gate.ts#audienceAllows` (`admitsBook`);\n - `dashboard.json` `widgets.requiresService` now points at `meta-item-read-gate.ts#filterDashboardForUser`.\n - Left alone: row 18 of `docs/adr/0056-permission-model-landing-verification.md` cites `rest-server.ts#filterAppForUser`. That is still a live declaration (the delegate REST's list route calls), and the file is a governed surface.\n - Results: `pnpm --filter @objectstack/spec run check:liveness` exit 0 (「758 pointer(s) written `path#symbol`, 758 naming a symbol the cited file contains」). `scripts/liveness/check-liveness.test.ts`: 64 passed, where 21 were red in CI. All `scripts/liveness/` tests: 252 passed (local project) and 81 passed (repo project).\n2. **`Clause-②: yes`.** `@objectstack/rest`'s only export subpath gains `createMetaItemReadGate` and five types: `MetaItemReadGateSources`, `MetaItemReadVerdict`, `MetaItemReadRefusal`, `MetaReadGateCaller` and `MetaReadGatePolicy`.\n - They are public because the runtime dispatcher consumes this one gate, and `@objectstack/rest` cannot import the runtime.\n - Precedent: #17672 / PR #17815.\n - The changeset now carries the declaration line and names the exports. The bumps are unchanged.\n - `node scripts/check-adr-0087-registration.mjs --base origin/main`: exit 0. `node scripts/check-changeset-no-major.mjs --base origin/main`: exit 0.\n3. **Composed-host pin, committed:** `packages/qa/http-conformance/src/hono-meta-item-read-gate.conformance.test.ts`, 7 cases. It boots a real `LiteKernel` and the real `createHonoApp`, and drives them with `app.request(...)`.\n - Non-holder: the doc, the doc's `/published` and the set-gated book answer `403`, with `error.code` `PERMISSION_DENIED`, `success: false` and no secret on the wire. The `crm` app answers `200`, pruned to `[nav_leads]`.\n - The holder control is served all four in full.\n - **Why this package and not `packages/adapters/hono`:** that package's `vitest.config.ts` aliases `@objectstack/runtime` to a stub (`src/__mocks__/runtime.ts`) for every test. `createHonoApp` imports `HttpDispatcher` from that specifier, so a host composed there would compose the stub. `http-conformance` is where the repo already boots the two for real (`hono-dispatcher-result-response.conformance.test.ts`): `@objectstack/hono` is aliased to source, and the runtime resolves through `dist/`, a ledgered pair in `check-test-source-alias`.\n - **Per-PR tier:** `@objectstack/http-conformance` is in `turbo ls --affected` for this diff, because it depends on `@objectstack/runtime` and `@objectstack/hono`. `Test Core`'s PR shards therefore collect it; they exclude only `@objectstack/dogfood`.\n - **Ablation (through `dist/`).** The mutate leg replaced the gate call `verdict = await judge(document);` with a serve-as-stored verdict carrying the marker `ablated20193`, then rebuilt the runtime. `ablation-dist-preflight` found the marker in `dist/index.js` and `dist/index.cjs`. Result: **4 failed | 3 passed**, exactly the four non-holder rows red and the holder controls green. The restore leg put the file back (blob equals `HEAD`, `git diff HEAD` empty), rebuilt, and `--absent` found no marker with a clean tree. Result: **7/7**. Both legs ran at `b58d036ed` and again at `acb99baa9`.\n\n**Verification at `acb99baa9`:**\n\n- runtime local tests: 281 files, 4016 passed, 1 skipped;\n- rest local tests: 201 files, 3576 passed, 1 skipped;\n- `@objectstack/hono`: 5 files, 122 passed; `@objectstack/http-conformance`: 7 files, 96 passed;\n- both typechecks: exit 0;\n- `pnpm lint`: exit 0;\n- `node scripts/check-issue-citations.mjs --base origin/main`: exit 0;\n- `dispatch-gates --commands` derived 77 commands, all ran, and `--ran` reconciled 77 of 77 with exit codes (0 NOT-MEASURED).\n\n## Changeset\n" }, { "find": "- The before-table was taken through `dispatch()`. Only the after-table was also re-read through a real `createHonoApp` app. A Hono-level *before* reading is NOT MEASURED: it would need an ablated `runtime` `dist` build, and `dispatch()` is the only thing the catch-all calls.", "replace": "- The before-table was taken through `dispatch()`. Through a real `createHonoApp` host, the four rows are now a committed pin (`hono-meta-item-read-gate.conformance.test.ts`). Its ablation leg, with the gate call removed and the runtime rebuilt, is the Hono-level *before* reading: the four non-holder rows go red, and the holder rows stay green." }, { "find": "so it is reported for the seat to file rather than folded in here.", "replace": "so it was filed separately as #20237 rather than folded in here. #20237 remains open." } ], "open_questions": [], "out_of_scope_findings": [ "carrier: seat (claim amendment) · noted, not filed — the amended claim names `packages/adapters/hono` for the composed-host pin, but that package's vitest.config.ts aliases @objectstack/runtime to a stub for every test, so no test there can compose the real dispatcher; the pin lives in packages/qa/http-conformance (deviation 1). Whether to amend the claim, or to add a second un-aliased vitest project in packages/adapters/hono, is the seat's call.", "carrier: maintainer (Tier H surface) · noted, not filed — docs/adr/0056-permission-model-landing-verification.md:70 row 18 cites `packages/rest/src/rest-server.ts#filterAppForUser`; it still resolves to a live declaration (a delegate into meta-item-read-gate.ts), so it is not rot, but the rule now lives in meta-item-read-gate.ts#filterAppForUserWithReason. Governed surface — not touched." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsACCEPT: PR #20236 at
acb99baa, R3 of thedomain:cliseat (after patch round 1)domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review of record, written 2026-09-27T15:15ZReviewed against GitHub and
origin/main, not against the report.Checklist:
- PR shape: draft, base
main. The first line isFixes #20193, and it is the body's only closing keyword; [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156, [finding] class closure: the rest of rest-server.ts's bare-Number() query reads. /history ?sinceSeq, /audit ?limit and /search ?perObject answer 200 on an unreadable value; close the family with one census pin #20139 and [finding] the runtime dispatcher's /meta LIST reads apply no per-caller gate: through a catch-all host, GET /meta/doc?include=content lists a set-gated doc body, /meta/book lists a set-gated book, /meta/app lists gated apps and entries #20237 are named without a closing verb. The only line-initial declaration isClause-②: yes, matching the changeset and the claim as amended in place. - Scope: 14 files, none governed (
check-governed-merges --pr 20236: NOT governed; 3581 changed lines, under the 5000-line bound):packages/rest/src/meta-item-read-gate.ts(new) and therest-server.tsdelegation;packages/rest/src/index.ts(one value and five type exports);packages/runtime/src/domains/meta.ts(gateMetaItemDocument);- two pin files and the census row edit;
- the four
packages/spec/liveness/{app,book,doc,dashboard}.jsonre-anchors (anchor andverifiedAtlines only); - the ADR-anchor JSON, the prose-id baseline and the changeset.
- Fix: route A, per ruling
5793362670item 1.RestServer's per-caller read gate moved byte-faithfully intocreateMetaItemReadGate, and bothRestServerand the runtime dispatcher's/metaitem and/publishedreads call it. That is one function with two callers, and there is no second audience resolver inpackages/runtime. - Tests:
meta-item-read-gate-parity.test.ts(77) pins dispatcher/RestServer parity;hono-meta-item-read-gate.conformance.test.ts(7) is the composed-host pin: a realLiteKernelbehind the realcreateHonoApp. Only identity is stubbed, and the runtime resolves throughdist/. It sits inpackages/qa/http-conformance, notpackages/adapters/hono: that package'svitest.config.tsaliases@objectstack/runtimeto a stub for the whole suite. The reviewer verified this deviation.- Ablation: a serve-as-stored verdict turns the 3 refusal rows and the app-prune row red, and the holder controls stay green.
- CI on head
acb99baa: 32 checkssuccessby latest name, includingTypeScript Type Check, allTest Coreshards,Spec property liveness,Lint & Repo GatesandCheck Changeset. 3 are skipped:Build Docs,Console Pin GateandPacked-tarball smoke (opt-in), each a rostered expected skip. - Contract review:
- round 0 FAIL (
5856229893) had three findings: the moved gate broke the liveness anchors (Test Core (1/6)red), the new@objectstack/restexports requireClause-②: yes, and a composed-host pin was missing. - delta PASS (
5857083250) found every re-anchored pointer true (check:liveness758/758 pointers).
Both were at-tier independent agents given only the card, the rulings and the PR.
- round 0 FAIL (
Decisions of record:
@objectstack/restminor+Clause-②: yes: its public export surface widens bycreateMetaItemReadGateand five types, in the form of A repeated?version=onGET /packages/:idis refused with404where the landed precedent for that exact condition on that exact route is400 VALIDATION_ERROR— and the one module that owns the rule is unreachable from the dispatcher package #17672 / PR fix(runtime): a repeated ?version= on GET /packages/:id answers 400 VALIDATION_ERROR from the one shared rule, and @objectstack/rest publishes it (#17672) #17815.@objectstack/runtimepatch: the dispatcher's read is pulled back to the declared per-caller contract (ADR-0046 §6.7). A permission pull-back is not Clause ②.
Out of scope: the dispatcher's LIST branch is filed as #20237 (
priority:p1,pm:blockedon this card). The pending decision on the partialappcells is #20156, since ruled B (5856774816) and queued behind this PR on the same gate file.Next: landing through the relay ops
pr_ready+automerge_enable.
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded: PR #20236 →
2bcd5cfeb70f0066dde256989bd56fa59c76b5b2domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· landing record, written 2026-09-27T15:49Z- Merged through the merge queue: enqueued 2026-09-27T15:17Z, merged 2026-09-27T15:47Z. The landing is a squash:
git rev-list --parents -n 1gives 2 fields. - Content reading on
2bcd5cfe:export function createMetaItemReadGatehas 1 hit inpackages/rest/src/meta-item-read-gate.ts;createMetaItemReadGatehas 3 hits inpackages/runtime/src/domains/meta.ts;- the composed-host pin
packages/qa/http-conformance/src/hono-meta-item-read-gate.conformance.test.tsand.changeset/20193-dispatcher-meta-read-gate.mdare in the tree.
- Negative control: on the parent
14ae40b0, both counts are 0 and both files are absent, so the instrument can fire. - Card: closed
completedby the PR'sFixes.pm:dispatchedand the assignee were cleared in this act. - Family status:
- The dispatcher's LIST branch is [finding] the runtime dispatcher's /meta LIST reads apply no per-caller gate: through a catch-all host, GET /meta/doc?include=content lists a set-gated doc body, /meta/book lists a set-gated book, /meta/app lists gated apps and entries #20237 (p1). Its
Blocked-by: #20193is satisfied by this landing, and it is unlocked in a separate act. - [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156's remainder (ruling B,
5856774816) edits this landing's gate module and waits on no other card. - Both, and [finding] class closure: the rest of rest-server.ts's bare-Number() query reads. /history ?sinceSeq, /audit ?limit and /search ?perObject answer 200 on an unreadable value; close the family with one census pin #20139, take the
rest-server.ts/meta-item-read-gate.tsfence serially.
- The dispatcher's LIST branch is [finding] the runtime dispatcher's /meta LIST reads apply no per-caller gate: through a catch-all host, GET /meta/doc?include=content lists a set-gated doc body, /meta/book lists a set-gated book, /meta/app lists gated apps and entries #20237 (p1). Its
Generated by Claude Code
- Merged through the merge queue: enqueued 2026-09-27T15:17Z, merged 2026-09-27T15:47Z. The landing is a squash:
- added 3 commits that reference this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site,
packages/runtime/src/domains/meta.tshandleMetadataRequest(the two-segment item branch and the/publishedbranch). Finding class (a), with the security exception: it exposes data.Found by the
os-devround on #20156 (PR #20190). Verified at source and filed by thedomain:cliexecution seat (#6024,session_01UYBdGBzWSrAMzpW8ah3GbP). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens (measured in-process by the #20156 dev)
The dev drove
handleMetadataRequestwith an authenticated caller who does not holdcrm_admin, on the same fixtures #20156's census uses:RestServerread answers the same callerGET /meta/doc/crm_admin_runbookPERMISSION_DENIEDGET /meta/doc/crm_admin_runbook/publishedGET /meta/book/admin_guide(a set-gated book)GET /meta/app/crmand/meta/app/crm/publishedrequiredPermissions-gated nav entry includedReach:
packages/runtime/src/domains/meta-verb-fallthrough.test.tspins that a realcreateHonoApphost (a${prefix}/*catch-all →dispatch()→ the domain registry → this handler) answersGET /api/v1/meta/object/accountfrom this read path. So a host that mounts only the catch-all serves these reads through the dispatcher. That leg is an existing in-repo measurement. The dev did not re-drive the doc/app requests through a composed host.The seat's reading at source (
origin/main, 2026-09-27)packages/runtime/src/domains/meta.tscarries no spelling of any of the per-caller gatesRestServerapplies:audience,filterApp,requiredPermissions,admitsBook,docReader,permissionSetall count 0. The controlhandleMetadataRequestcounts 2 in the same file.packages/runtime/src/http-dispatcher.tsmentionsaudienceonly in a docblock about permission-set suggestion bindings (:2145, :2149), not a read gate.The contract it contradicts
content/docs/ui/apps.mdx, therequiredPermissionsrow: 「The entry is never served: it is absent from the/metabody」./metaread with different rules.Why it is not #20156's close
#20156 is "an alternate door skips the plain read's gate". Here the transport has no gate at all, its plain read included. Porting the gates into
packages/runtimewould be a second audience resolver, which ruling5793362670item 1 forbids (「⛔ no second resolver」). The #20156 dev leftpackages/runtimeuntouched and raised the remedy as a decision.The remedy options the dev set out (⛔ not chosen here)
RestServer's onemetaItemReadGate, docs-audience resolution included) to one transport-neutral seam thatRestServerandhandleMetadataRequestboth call./metaitem reads soRestServeris the one owner (AGENTS.md "Route & surface ownership", rule 1), and mount REST in the catch-all-only hosts.packages/runtimeas a second resolver, is ruled out by5793362670.)The dev recommends B if the catch-all-only hosts can mount REST, otherwise A. That depends on a measurement this card owes first: which shipped hosts are catch-all-only.
Who acts
The
domain:cliexecution seat (packages/runtimeandpackages/restare both this lane's), after triage grades the card and, if the options really diverge, after the placement is decided. It builds on PR #20190 (#20156), which introduces the shared gate A would extract, so it waits for that PR to merge.Dedupe
MCP issue search in this repository, open and closed, run 2026-09-27:
handleMetadataRequest runtime dispatcher meta audience gate docs permission set→ 3 hits, all closed: [finding] the metadata TYPE registry moves the same metadataForms bundles and is still invisible to dispatch-gates path derivation #9144 (the dispatch-gates path derivation), meta apps by-name route: answer an explicit permission-denied envelope for an unauthorized session, instead of being indistinguishable from "not published" (backend half of objectui#4252) #8013 (the RestServer by-name app envelope) and A bearer-authenticated admin metadata write is stampedactor: 'system'—req.user/req.userIdare unset on the/metaPUT path #7749 (the/metaPUT actor stamp). None is this defect.Dedupe words:
dispatcher meta audience gate·createHonoApp meta doc permissionSet leak·handleMetadataRequest app requiredPermissions unfiltered·catch-all meta ungatedGenerated by Claude Code