Repository navigation
[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
Description
Ruled: 5856774816 · letter B · 2026-09-27T14:47Z
Filing gate: ① a defect with a named landing site, packages/rest/src/rest-server.ts: serveMetaItemLayered and the /meta/:type/:name/published handler. Finding class (a), with reach: measured at a public HTTP door; security exception: it leaks data. Filed as the family's class-closure card: #20130 is the first member (the app document), and this is the second.
Found by the os-dev round on #20129 (PR #20155), and verified at source and filed by the domain:cli execution seat (#6024, session_01UYBdGBzWSrAMzpW8ah3GbP). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens (measured)
The dev drove the real route handlers on origin/main 16c5a33fdd with a protocol double that implements getMetaItemLayered. All reads were healthy. The caller is authenticated and does not hold crm_admin. crm_admin_runbook is a doc that only a { permissionSet: 'crm_admin' } book claims.
| door | answer |
|---|---|
GET /meta/doc/crm_admin_runbook (control) |
403 PERMISSION_DENIED |
GET /meta/doc/crm_admin_runbook/layers |
200, the gated body in code / overlay / effective |
GET /meta/doc/crm_admin_runbook?layers=true (deprecated flag) |
200, the same body |
GET /meta/doc/crm_admin_runbook/published |
200, the gated body |
The seat's reading at source (origin/main 8d1f7ab7):
serveMetaItemLayeredis reached from both layered entry points (the/layersroute and the?layers=branch of the plain read). It applies the ADR-0106 object-schema mask and nothing else. There is no ADR-0046 §6.7 audience arm.isAudienceGatedType(metaType === 'book' || metaType === 'doc') is read only by the cache exclusion and by the uncached single-item branch's gate. Neither alternate door consults it.
The class, and the enumeration the closure pin owes
The plain single-item read GET /meta/:type/:name applies a per-caller read gate for three types. The alternate doors that serve the same document skip all of them:
| gate on the plain read | type | /layers + ?layers=true |
/published |
/history, /diff |
|---|---|---|---|---|
| ADR-0046 §6.7 docs audience | doc |
measured: leaks the body | measured: leaks the body | NOT MEASURED |
| ADR-0046 §6.7 docs audience | book |
NOT MEASURED | NOT MEASURED | NOT MEASURED |
nav filter (filterAppForUserWithReason) |
app |
measured on #20130 | measured on #20130 | code-read on #20130 |
service gate (filterDashboardForUser) |
dashboard |
code-read only: absent from serveMetaItemLayered |
NOT MEASURED | NOT MEASURED |
⇒ The close is one census pin over gated type × alternate door. For each cell, the alternate door answers what the plain read answers for the same caller, or refuses. Every door then runs the gate as a reader of one shared spelling, rather than each door gaining its own copy. #20130 is this family's app row, and triage decides whether it folds in here or stays a member card.
The contract it contradicts
- ADR-0046 §6.7: docs content is gated server-side at the doc and book reads.
- The by-name route's own rule, quoted on [finding] GET /meta/app/:name/layers, /published, /history and /diff serve the app document with no nav filter, so entries pruned by requiredPermissions and the docs audience reach every member #20130: 「the by-name route must not serve a nav entry the list route prunes, or reading the single-app JSON defeats the filter」. This card is the same lesson for the doc body. The leaked bytes here are the gated content itself, not entry names.
Reach and context
- Not the [finding] the docs reads fail OPEN when the book list read throws: fetchAudienceBooks maps the fault to [], so a permission-set-gated doc is served (200, with its body) to an authenticated non-holder #20129 defect. That card is a gate input read as absent on a store FAULT. Here the gate is ABSENT on healthy reads. PR fix(rest): docs reads fail closed when the book or corpus read faults #20155 fixes [finding] the docs reads fail OPEN when the book list read throws: fetchAudienceBooks maps the fault to [], so a permission-set-gated doc is served (200, with its body) to an authenticated non-holder #20129 only and leaves these doors unchanged.
- Who is exposed: any authenticated member of the environment can read any set-gated doc through these three doors. Anonymous callers were not measured on them.
- Scope question for triage, ⛔ not presumed here: these doors are authoring and diagnostics surfaces. Pulling them back to the plain read's per-caller answer keeps them open to every member. Refusing them to non-authors would be a new permission boundary. [finding] GET /meta/app/:name/layers, /published, /history and /diff serve the app document with no nav filter, so entries pruned by requiredPermissions and the docs audience reach every member #20130's execution note already picked the former for the app row.
Who acts
The domain:cli execution seat. After triage grades it, the card waits behind the lane's in-flight R1 (#20129, serial dispatch), since it edits the same file.
Dedupe
MCP issue search in this repository, open and closed, run 2026-09-27:
doc layers published side door audience gate permission set leak meta doc→ 4 hits. [finding] GET /meta/app/:name/layers, /published, /history and /diff serve the app document with no nav filter, so entries pruned by requiredPermissions and the docs audience reach every member #20130 (open) is this family's app row. [finding] the docs reads fail OPEN when the book list read throws: fetchAudienceBooks maps the fault to [], so a permission-set-gated doc is served (200, with its body) to an authenticated non-holder #20129 (open) is the fault-path card. 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 (closed) is the by-name app envelope. ADR-0089 D3a: flip.strict()on view/page schemas so mis-layered visibility keys are loud errors #2902 (closed) is unrelated.serveMetaItemLayered audience ADR-0046 §6.7 layered view gated doc body→ 5 hits, none this defect. getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」 #5707 (closed) is the layered overlay's bare catch; [Decision]GetMetaItemRequestSchema(:271) andGetMetaItemLayeredRequestSchema(:441) organizationId describes are now measurably FALSE — correct them, or leave all of them until ADR-0131 C5 deletes the surface? #16524, ADR-0105 D10: layered master data needs its follow-up ADR — the one remaining Phase 2 framework design item #4585, ADR-0105 D10: layered master data (group template + org override) — awaiting its follow-up ADR #4030 and ADR-0089 D3a: flip.strict()on view/page schemas so mis-layered visibility keys are loud errors #2902 are unrelated.meta doc history diff published endpoint bypasses per-caller read gate documentation audience→ 18 hits, 15 read. Only [finding] the docs reads fail OPEN when the book list read throws: fetchAudienceBooks maps the fault to [], so a permission-set-gated doc is served (200, with its body) to an authenticated non-holder #20129 and [finding] GET /meta/app/:name/layers, /published, /history and /diff serve the app document with no nav filter, so entries pruned by requiredPermissions and the docs audience reach every member #20130 touch these doors. [finding]metadata.endpoints.itemgates four read doors but NOT the per-item writes (PUT/DELETE) or the history family — the #15542 radius mismatch, one switch over #15854 (closed) is themetadata.endpoints.itemradius, a different gate. [finding] The/meta/:type/:name/publisheddoor argues in a comment that a RAW tenant "is right for a READ" — the argumentorganizationIdForMetaReadexists to refute, and the door the #14907 card recorded as already gating #16032 (closed) is the/publishedtenant argument.
Dedupe words: doc layers audience bypass · meta doc published permissionSet leak · serveMetaItemLayered audience gate · side door per-caller read gate
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 27, 2026
Path: permissions that actually hold | access-security.fls-mask-and-strip | P2
Triage: first grade (class-closure card; #20130 folded in) —
bug·security·priority:p1·domain:cli·area:access·pm:queueTriage: lands in
packages/rest/src/rest-server.ts(serveMetaItemLayered, reached from/layersand the?layers=branch, plus the/published,/historyand/diffhandlers) ⇒domain:cli. Rationale: the plainGET /meta/:type/:nameapplies a per-caller read gate fordoc/book(ADR-0046 §6.7 audience),app(nav filter) anddashboard(service gate). The alternate doors that serve the same document skip every one of them. Measured at the public HTTP door by the #20129 dev: an authenticated non-holder gets 200 with the permission-set-gated doc body from/layers,?layers=trueand/published, while the plain read answers 403. That is gated content leaking on healthy reads, not a fault path. The sibling #20129 (the same gate failing open on a store fault) was p1 ⇒ p1.reach:measured, plus the data-exposure exception.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T04:13Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), #20130 and its triage comment5830552645, #20129 (closed; PR #20155 merged at 2026-09-27T03:07Z), andorigin/main2bbebf5.#20130 is folded here. It is the same file, the same mechanism (an alternate door skipping the plain read's gate) and the same fix: one census pin over gated type × alternate door. It was unclaimed, with no open PR, and a family goes into its closure card, so #20130 closes as a duplicate of this card in the same act. Carried from it:
/layers,/published,/historyand/diffserve the app document with nofilterAppForUserWithReason, so entries pruned byrequiredPermissionsand the docs audience reach every member, againstcontent/docs/ui/apps.mdx(「The entry is never served」);/meta/appto the runtime'shandleMetadataRequest. If that path answers, it gets the same gate.Direction (the one #20130 already set, and not a new ruling): pull every alternate door back to the plain read's per-caller answer, and route each door through one shared gate spelling rather than a copy per door. ⛔ Refusing these doors to non-authors instead would be a new permission boundary. That is the maintainer's call, not this card's.
Execution notes.
doc,book,app,dashboard) × door (/layers,?layers=true,/published,/history,/diff), the door answers what the plain read answers for the same caller, or refuses. Measure the NOT MEASURED cells first, and state the anonymous-caller reading.rest-server.ts(checked this act), and [finding] the docs reads fail OPEN when the book list read throws: fetchAudienceBooks maps the fault to [], so a permission-set-gated doc is served (200, with its body) to an authenticated non-holder #20129's PR fix(rest): docs reads fail closed when the book or corpus read faults #20155 has merged.objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 3 (batch 3 under
seat-lifecycle.mdas of0a6024d10; R2's PR #20164 is in the merge queue)Session:
session_01UYBdGBzWSrAMzpW8ah3GbPAccount:
os-litantBranch:
claude/issue-20156-alternate-door-read-gatesWorktree:
objectstack-issue-20156Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/rest/src/rest-server.ts(the alternate read doors of/meta/:type/:name:serveMetaItemLayeredand its two entry points, the/published,/history,/diffand/audithandlers (/auditamended in place by the seat at review: it is the same stored-version class as/history, found by the census reading the door list off the route table), and the plain read's per-caller gates they must share: the docs audience gate,filterAppForUserWithReasonandfilterDashboardForUser),packages/runtime/src/domains/meta.ts(handleMetadataRequest, only if its measurement shows it answers these reads without the gates), their tests underpackages/rest/src/andpackages/runtime/src/, any hand-writtencontent/docs/**line the change makes false, and one.changeset/20156-*.md.packages/spec/**is read-only.CHANGELOG.mdandpackage.jsonfiles belong to the Version Packages PR. Stop on breach and explain in the reportContainer & model:
M,mode:subagent,model: default judgment tier(this act'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tieroverpackages/rest/src/rest-server.tsandpackages/runtime/src/domains/meta.tsanswered: no path-derived mandate). This is a permission-boundary census across four gated types and five doors, built on one shared gate spelling, so it gets the default judgment tier. The at-tier contract review follows delivery.Clause-②: no
Thread-read: 5852543510
Serial constraints cleared:
No open PR touches packages/rest/src/rest-server.ts or packages/runtime/src/domains/meta.ts: the file lists of all 10 open non-release PRs were read in this act, and the one packages/rest hit (PR #20174) is packages/rest/src/aggregation-filter-where-doors.test.ts. rest-server.ts has been free since 9401b842 (#20129, this seat's R1). The lane sibling #20139 shares rest-server.ts; fold-or-serial answered SERIAL (fold gate ① fails: a different defect and fix), so it waits for this card's merge. #20130 was folded into this card by triage (closed as a duplicate). The H17 trigger-file index names no hold on these two files. R2's PR #20164 touches only packages/cli. The NORTH-STAR 优先级 3 pre-pick: this card is the domain:cli pm:queue's only open P1Taken 2026-09-27T05:38Z,
origin/main9347c1f2.Clause-②: no: the change pulls the alternate doors back to the per-caller answer the plain read already gives. That narrows what a non-holder can read to the declared gates, and a runtime permission/security behaviour change is outside clause ② (references/execution-duties.md, negative boundary). Triage's direction is carried, not re-ruled: ⛔ refusing these doors to non-authors instead would be a NEW permission boundary, the maintainer's call. A route that needs one stops the dev atneeds_decision, as does one that needs a new error code. The dev posts no second claim; this comment is its identity on this card.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{ "issue": 20156, "status": "done", "branch": "claude/issue-20156-alternate-door-read-gates", "pr": "https://github.com/objectstack-ai/objectstack/pull/20190", "session": "session_01UYBdGBzWSrAMzpW8ah3GbP — the dispatch's session, stamped in every commit's Claude-Session trailer", "premise_still_valid": true, "summary": "The plain GET /meta/:type/:name read's per-type gates (app nav filter, ADR-0057 D10 dashboard widget gate, ADR-0046 §6.7 docs audience) now live in ONE function, RestServer#metaItemReadGate, which the plain read and every door beside it ask. Door dispositions: /published answers exactly what the plain read answers (every arm, pruned or masked). /layers, ?layers=true and /diff serve stored versions: the plain read's refusal where it refuses, 403 PERMISSION_DENIED where the plain read would serve only part of an app (the designer saves back what it loads), per-caller arms only. /history and /audit answer the plain read's refusal where it refuses the item whole. The ADR-0106 object mask, a fourth per-caller gate the doors skipped, now also covers /published and /diff. A census (gated type x door x caller, door list read off the route table) went from 35 red to 0. The runtime dispatcher's /meta was measured ungated even on its plain read. It is reported as a finding needing a decision, not patched here. | files_changed: packages/rest/src/rest-server.ts; packages/rest/src/meta-alternate-door-read-gates.test.ts (new census); packages/rest/src/execctx-consumer-census.test.ts (re-measured counts); content/docs/ui/apps.mdx (one paragraph); .changeset/20156-alternate-door-read-gates.md (patch, @objectstack/rest). 5 files, +1196/-181. | deviations: (1) Zone 2 item 2 falsified. The dashboard widget gate is per-DEPLOYMENT, not per-caller (the plain read's own cache-exclusion comment says so), so the stored-version doors do not apply it or the app's requiresService/servability arms. /published keeps them. (2) Zone 3 route changed. A partly-withheld app is REFUSED, not served pruned, on the stored-version doors. Reason: content/docs/plugins/adding-a-metadata-type.mdx documents DesignerEditorWrapper loading ?layers=true and saving what it loaded, so a pruned version saved back silently deletes the withheld entries (see open question 1). (3) A fourth per-caller gate was found: the ADR-0106 object mask, skipped by /published and /diff. Both now route through it, per ADR-0106 D5(4). (4) /audit was gated too and /references declared exempt: the census door list is read off the route table, and /audit is the same class as /history. /audit is outside the claim's enumerated handlers and inside its 'alternate read doors of /meta/:type/:name'. The seat may want to amend the claim's file surface. (5) Zone 2 item 5: handleMetadataRequest answers these reads without gates, but its PLAIN read is ungated too. Gating it needs the gate extracted to a transport-neutral seam or the twin retired, because a runtime port would be the second resolver the #19790 ruling forbids. packages/runtime was left untouched and this is reported as out-of-scope finding 1 plus open question 2. The PR still says Fixes #20156 as dispatched; if the seat counts the dispatcher half as in-card, switch it to Part of. (6) The add_repo read of objectstack-ai/objectui was refused by the permission classifier, so Studio's consumer code was not read; the designer round-trip rests on the in-repo docs page. (7) /diff of a doc/book/app with no current document now answers the plain read's 404 RESOURCE_NOT_FOUND. | line_budget: n/a — no skills/** diff.", "tests": "RED (at db4de902, census only, before the fix): 190 cases, 35 failed. doc non-reader on all six doors, book non-reader on all six, app crm non-reader on layers/?layers/published/diff, app payroll + launchpad non-reader on all six, dashboard on /published, object non-reader on /published + /diff, and anonymous x ?layers=true x doc and book = 200 with the body (including the org doc the plain read answers 401). Plain-read rows and the route-table row were green. GREEN (at ace7e1df): census 194/194 (4 edge pins added). ABLATION: fix committed first, then scripts/ablation-replace.mjs replaced /published's metaItemReadGate call with an ungated serve. Anchor x1 -> x0, blob 524c67ca -> 5a8bfcbd. The /published row went red on 7 cells (every gated cell; the object cell stayed green because the mask is its own seam). Restore: blob 524c67ca == HEAD blob, git diff HEAD empty. The subject is imported relatively from src, so no dist was involved. RUNTIME DISPATCHER PROBE (in-process, handleMetadataRequest, authenticated non-holder, scratch file deleted): GET /meta/doc/crm_admin_runbook -> status 200 · doc body served: true · gated nav entry served: false · gated book served: false | GET /meta/doc/crm_admin_runbook/published -> status 200 · doc body served: true · gated nav entry served: false · gated book served: false | GET /meta/book/admin_guide -> status 200 · doc body served: false · gated nav entry served: false · gated book served: true | GET /meta/app/crm -> status 200 · doc body served: false · gated nav entry served: true · gated book served: false | GET /meta/app/crm/published -> status 200 · doc body served: false · gated nav entry served: true · gated book served: false. Host reach (catch-all-only createHonoApp) comes from the pinned in-repo measurement in packages/runtime/src/domains/meta-verb-fallthrough.test.ts and was not re-driven here. GATES (command -> exit; union run at ace7e1df): node scripts/check-adr-0087-registration.mjs --base origin/main -> 0 ;; node scripts/check-adr-0087-registration.mjs --self-test -> 0 ;; node scripts/check-changeset-no-major.mjs --base origin/main -> 0 ;; node scripts/check-changeset-no-major.mjs --self-test -> 0 ;; node scripts/check-ci-filter-parity.mjs -> 0 ;; node scripts/check-closing-keyword-parity.mjs -> 0 ;; node scripts/check-closing-keyword-parity.mjs --self-test -> 0 ;; node scripts/check-comment-mask-adoption.mjs -> 0 ;; node scripts/check-comment-mask-adoption.mjs --self-test -> 0 ;; node scripts/check-comment-mask-corpus.mjs -> 0 ;; node scripts/check-doc-frontmatter.mjs -> 0 ;; node scripts/check-doc-frontmatter.mjs --self-test -> 0 ;; node scripts/check-doc-route-spelling.mjs --advisory -> 0 ;; node scripts/check-doc-route-spelling.mjs --self-test -> 0 ;; node scripts/check-docs-section-name.mjs -> 0 ;; node scripts/check-docs-section-name.mjs --self-test -> 0 ;; node scripts/check-empty-changeset.mjs --base origin/main -> 0 ;; node scripts/check-empty-changeset.mjs --self-test -> 0 ;; node scripts/check-keyed-text-bounds.mjs -> 0 ;; node scripts/check-keyed-text-bounds.mjs --self-test -> 0 ;; node scripts/check-platform-object-tenancy-census.mjs -> 0 ;; node scripts/check-platform-object-tenancy-census.mjs --self-test -> 0 ;; node scripts/check-plugin-teardown-shape.mjs -> 0 ;; node scripts/check-plugin-teardown-shape.mjs --self-test -> 0 ;; node scripts/check-registry-log-declared.mjs -> 0 ;; node scripts/check-registry-log-declared.mjs --self-test -> 0 ;; node scripts/check-rest-log-spy-declared.mjs -> 0 ;; node scripts/check-rest-log-spy-declared.mjs --self-test -> 0 ;; node scripts/check-section-landing-index.mjs -> 0 ;; node scripts/check-section-landing-index.mjs --self-test -> 0 ;; node scripts/check-system-context-census.mjs -> 0 ;; node scripts/check-system-context-census.mjs --self-test -> 0 ;; node scripts/check-undeclared-dep-imports.mjs -> 0 ;; node scripts/check-undeclared-dep-imports.mjs --self-test -> 0 ;; node scripts/docs-audit/check-affected-docs.mjs -> 0 ;; node scripts/docs-audit/check-drift-comment.mjs -> 0 ;; node scripts/pm/release-rehearsal-clone.mjs --self-test -> 0 ;; pnpm --filter @objectstack/lint run check:doc-formula-expressions -> 0 ;; pnpm --filter @objectstack/lint run check:doc-security-posture -> 0 ;; pnpm --filter @objectstack/spec run check:docs -> 0 ;; pnpm --filter @objectstack/spec run check:duration-unit-keys -> 0 ;; pnpm --filter @objectstack/spec run check:empty-state -> 0 ;; pnpm --filter @objectstack/spec run check:liveness -> 0 ;; pnpm --filter @objectstack/spec run check:skill-examples -> 0 (first run exit 3 PREREQUISITE NOT MET on unbuilt client/client-react; built them, re-run exit 0) ;; pnpm --filter @objectstack/spec run check:strictness-ledger -> 0 ;; pnpm --filter @objectstack/spec run check:variant-docs -> 0 ;; pnpm --filter @objectstack/spec run check:yaml-examples -> 0 ;; pnpm check:authz-resolver -> 0 ;; pnpm check:changeset-gate-self-tests -> 0 ;; pnpm check:corpus-claim-drift -> 0 ;; pnpm check:cross-package-test-inputs -> 0 ;; pnpm check:dispatcher-error-vocabulary -> 0 ;; pnpm check:doc-anchors -> 0 ;; pnpm check:doc-authoring -> 0 ;; pnpm check:docs-audit-scope -> 0 ;; pnpm check:docs-redirects -> 0 ;; pnpm check:docs-single-h1 -> 0 ;; pnpm check:docs-spec-enumerations -> 0 ;; pnpm check:docs-transcript-drift -> 0 ;; pnpm check:driver-memory-census -> 0 ;; pnpm check:dts-closure -> 0 ;; pnpm check:dual-build-cjs-loads -> 3 = NOT MEASURED (PREREQUISITE NOT MET: needs the full package farm built; CI builds it) ;; pnpm check:engine-double-contract -> 0 ;; pnpm check:gitlink-declared -> 0 ;; pnpm check:issue-citations -> 0 ;; pnpm check:lean-entry-closure -> 0 ;; pnpm check:logger-receiver-detach -> 0 ;; pnpm check:nul-bytes -> 0 ;; pnpm check:objectql-double-limit -> 0 ;; pnpm check:objectui-changeset -> 0 ;; pnpm check:org-identifier -> 0 ;; pnpm check:page-declaration-shape -> 0 ;; pnpm check:pm-changeset-deadline-census -> 0 ;; pnpm check:published-files -> 0 ;; pnpm check:published-readme-links -> 0 ;; pnpm check:query-options-erasure -> 0 ;; pnpm check:react-page-adapter-contract -> 0 ;; pnpm check:refd-timer-probe -> 0 ;; pnpm check:role-word -> 0 ;; pnpm check:route-envelope -> 0 ;; pnpm check:skill-identifier-liveness -> 0 ;; pnpm check:slot-lookup -> 0 ;; pnpm check:sourcemap-no-sources-content -> 0 ;; pnpm check:test-source-alias -> 0 ;; pnpm check:tier-file-adoption -> 0 ;; pnpm check:type-check-coverage -> 0 ;; pnpm check:type-check-debt -> 3 = NOT MEASURED (PREREQUISITE NOT MET: needs the full package farm built; CI builds it) ;; pnpm check:vendor-version-stamps -> 0 ;; pnpm check:watch-hint-literal -> 0 ;; pnpm check:where-matcher -> 0 ;; pnpm lint -> 0 (repo-wide eslint . --no-inline-config, 109s, at ace7e1df) ;; pnpm --filter @objectstack/rest exec vitest run --project local -> 0 (197 files: 3518 passed, 1 skipped, 0 failed, at ace7e1df) ;; pnpm --filter @objectstack/rest typecheck -> 0 (tsc --noEmit + check:test-typecheck, at ace7e1df) ;; node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list -> 0 (90 derived, 88 run, 2 NOT-MEASURED, 0 UNRUN)", "mcp_calls": "1 — mcp__Claude_Code_Remote__add_repo (objectstack-ai/objectui, access: read), refused by the permission classifier before it ran. 0 GitHub MCP calls, 0 MCP write tools.", "api_writes": "3 — each one POST /repos/objectstack-ai/objectstack/dispatches (the fleet-write relay, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft PR 20190, run 36301886858); (2) label-write assign -> POST /repos/objectstack-ai/objectstack/issues/20190/assignees [os-litant] (run 36301922107; readback matches, labels untouched); (3) this os-dev-report -> POST /repos/objectstack-ai/objectstack/issues/20156/comments. Plus git push of the branch (not REST).", "open_questions": [ { "question": "On the stored-version doors (/layers, ?layers=true, /diff), what should a caller get when the plain read would serve them only PART of an app (entries withheld by requiredPermissions or the docs audience)? This PR refuses (403 PERMISSION_DENIED). The dispatch's suggested route was to serve it pruned.", "options": [ "A — keep refuse-on-partial (this PR). It is loud and adds no boundary. Cost: an author missing one entry's permission cannot open that app's layered view or diff.", "B — writer exemption in ADR-0106 D4's shape: callers that metaWriteCapabilityVerdict admits see the whole stored app on the stored-version doors. This is a NEW permission boundary, so it is the maintainer's call.", "C — serve pruned (the suggested route) plus a write-side guard that refuses a PUT dropping entries withheld from the writer. It is a write-door change and a new refusal, so it needs its own card." ], "recommendation": "A now; B only on a maintainer ruling. Business need: the documented consumer (DesignerEditorWrapper via ?layers=true) saves what it loads, so a pruned load is a silent deletion. This rests on the in-repo docs; objectui source was not readable. Long-term: B is the ADR-0106 'whoever may write sees all of it' invariant carried over, which is exactly why it needs a ruling rather than a dev's choice. AI-safety: a loud 403 beats a silent deletion that an AI author would never notice. Startup scope: A adds no boundary, no write-side machinery and no new gate." }, { "question": "The runtime dispatcher's /meta (handleMetadataRequest) answers GET /meta/:type/:name and /published with NO per-caller gate, the plain read included. It is the front door in catch-all-only hosts (the createHonoApp embed). Which remedy?", "options": [ "A — extract the per-caller read gate, docs-audience resolution included, to one transport-neutral seam that RestServer and handleMetadataRequest both call. One resolver, two transports.", "B — retire the dispatcher's /meta item reads so RestServer is the one owner (Route & surface ownership rule 1), and mount REST in the catch-all-only hosts.", "C — port the gates into packages/runtime. This is a second resolver, which the #19790 ruling (item 1) forbids, so it is ruled out." ], "recommendation": "B if the catch-all-only hosts can mount REST; otherwise A. Business need: measured leak on a documented embed shape. Long-term: B removes a twin whose every invariant forks; A keeps the twin but shares the gate. AI-safety: one owner means an AI cannot add a door on the ungated transport. Startup scope: B shrinks the surface, A adds a seam. It needs its own card either way; the seat files it." } ], "out_of_scope_findings": [ "class: a · reach: exception: security (data exposure) — measured in-process on handleMetadataRequest; the host reach is the pinned in-repo measurement (meta-verb-fallthrough.test.ts, catch-all-only createHonoApp) and was not re-driven · Seam: spec:ADR-0046 §6.7 docs audience + app requiredPermissions → runtime:packages/runtime/src/domains/meta.ts handleMetadataRequest (the two-segment item branch and the /published branch) · evidence: an authenticated non-holder gets 200 with the permission-set-gated doc body on /meta/doc/NAME and /meta/doc/NAME/published, the gated book on /meta/book/NAME, and a requiredPermissions entry on /meta/app/NAME and /published, where RestServer answers 403 / pruned · not family-closure material for #20156 (there, a door skips the plain read's gate; here, the transport has no gate at all), and it needs open question 2's decision · dedupe words: dispatcher meta audience gate · createHonoApp meta doc permissionSet leak · handleMetadataRequest app requiredPermissions unfiltered · catch-all meta ungated", "carrier: 承接者:无 · noted, not filed — GET /meta/:type/:name/references serves the type/name/label of OTHER items that reference this one, applying none of those items' own gates (for example an unpublished app, which ADR-0045 §3 holds externally unobservable). Not measured, outside this card's class, declared exempt in the census with its reason; recorded in the PR's Acceptance notes only." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
REWORK (patch round 1): PR #20190 at
ace7e1df, with the app row of the stored-version doors split out for a maintainer rulingdomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review verdict, written 2026-09-27T07:35ZThe at-tier independent contract review answered FAIL; record
5853855259is on the PR. The seat verified its three required changes and upholds them.Lint & Repo Gatesfails at the board check 「Issue citations this change adds resolve on the board」:#6241and#10340sit on lines the PR moved into the shared gate, and neither resolves on the board./diffrequires every entry permission. The review measured it in a worktree: a platform admin'ssystemPermissionsis the fixed seven-item capability list, with no wildcard, andfilterAppForUserWithReasonhas no admin exemption. So for an app withrequiredPermissions: ['finance.access'], the plain read answers 200 pruned while/layersand?layers=trueanswer 403. A platform admin could not open the Studio designer (which loads?layers=true) on such an app. Triage (5852543510) and this card's claim reserve a new boundary to the maintainer, so the seat does not rule it. The seat corrects its own earlier leaning here: it had read the refusal as within 「the plain read's answer, or a refusal」 and so the seat's to rule. The measured admin lockout shows otherwise.view,flow, and others) are untouched on every door".dashboard's/publishednow applies the ADR-0057 D10 widget gate.Seat sequencing (⛔ not a ruling on the boundary): the p1 content leak (
docandbookbodies, and object fields on/publishedand/diff) must not wait on a product decision./publishedon every arm, the object mask,/historyand/auditrefusing where the plain read refuses the item whole./layers,?layers=trueand/diff. Those three app cells return to their pre-PR answer, the [finding] GET /meta/app/:name/layers, /published, /history and /diff serve the app document with no nav filter, so entries pruned by requiredPermissions and the docs audience reach every member #20130 entry-name exposure that triage graded p2 there.Part of #20156.The runtime dispatcher half is filed as #20193. Patch round to the same dev, on the same claim and branch.
Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{ "issue": 20156, "status": "done", "branch": "claude/issue-20156-alternate-door-read-gates", "pr": "https://github.com/objectstack-ai/objectstack/pull/20190", "session": "session_01UYBdGBzWSrAMzpW8ah3GbP — the dispatch's session, stamped in every commit's Claude-Session trailer", "premise_still_valid": true, "summary": "PATCH ROUND 1 (rework on review 5853855259 and seat verdict 5853865127), pushed head 8d314ac67. (1) The two unresolvable citations the shared gate moved (#6241 and #10340) now name their landing commits, 83a3b1f2e and 26f3588fb (each verified an ancestor of origin/main), and say the original card no longer resolves. The diff-scoped board check reads 0 findings. (2) The refuse-on-partial app arm is REMOVED WHOLE rather than left dormant: filterAppForUserWithReason and resolveNavDocAudience are byte-identical to base again. The gate takes a mode flag, MetaReadGatePolicy.app = 'gate' | 'pending-decision'. The stored-version doors (/layers, ?layers=true, /diff) share one STORED_VERSION_DOOR_POLICY with the app arm off, so those cells serve the stored app exactly as before the PR, and /diff no longer fetches the current document for app. The census declares those cells (18: app x 3 doors x 2 signed-in callers x 3 fixtures) as PENDING-DECISION #20156: each asserts the pre-PR answer, and a separate test holds the exemption to exactly that row. Every other arm stands: doc/book audience on every door, /published on every arm, the object mask on /published and /diff, and /history and /audit refusing where the plain read refuses the item whole. (3) The changeset was rewritten: dashboard's /published now applies the ADR-0057 D10 widget gate; the three stored-version doors are unchanged for app in this release, pending a decision that restricts authors either way, platform admins included; no refuse-on-partial claim remains. apps.mdx says the same. (4) The PR body is NOT patched by me. The full replacement body is saved for the seat: first line 'Part of #20156', '#20193 remains open' named only that way, and the review's census-scope note added to Acceptance notes. origin/main was merged twice (a733beb2e, 8d314ac67), with no conflicts and no upstream overlap with the diff. | files_changed (vs merge base 560b724c9): packages/rest/src/rest-server.ts; packages/rest/src/meta-alternate-door-read-gates.test.ts; packages/rest/src/execctx-consumer-census.test.ts; content/docs/ui/apps.mdx; .changeset/20156-alternate-door-read-gates.md. 5 files, +1245/-172. Round-1 commits: 92e6b2fca plus merges a733beb2e and 8d314ac67. | deviations (round 1): (a) The coordinator asked to 'drop the refuse-on-partial arm'. I read 'those three app cells return to their PRE-PR answer' plus 'a mode flag that turns the app arm off' as the whole app row of the three stored-version doors, all callers. So payroll's 403 and launchpad's 404 on /layers, ?layers=true and /diff also return to the pre-PR stored answer, because a platform admin lacking an app-level requiredPermissions would be locked out of the designer the same way. /history and /audit keep the app whole-refusal, as instructed. If the seat meant only the partial cells, the flag is the one place to change. (b) The fault edge pin was rewritten: with the app arm off on stored-version doors, it now pins a doc gate-input fault answering the plain read's 503 on all six doors, plus the app /published half. (c) origin/main advanced again (3 commits) after the second merge. Not merged a third time; the gate derivation reports none of the commits it can see touches what it derives from. (d) Round-0 deviations 1, 3, 5, 6 and 7 stand; round-0 deviation 2 (refuse-on-partial) is withdrawn. /diff's absent-item 404 now covers doc and book only. | pr_body_change: full replacement body saved at /tmp/claude-0/-home-user-objectstack/df3d1045-8eb6-5b68-98d9-141980edea33/scratchpad/issue-20156/pr-body-r1.md (9110 bytes; lint-clean: no angle brackets, no closing keyword near a number, no control bytes). The diff against the live body is at /tmp/claude-0/-home-user-objectstack/df3d1045-8eb6-5b68-98d9-141980edea33/scratchpad/issue-20156/pr-body-r1.diff. The live body was verified byte-identical to what round 0 sent before this was drafted. Minimal find/replace if the seat prefers pairs: line 1 'Fixes #20156' -> 'Part of #20156'. The rest of the body also changes (refuse-on-partial sections removed, pending-decision section and round-1 numbers added), so the full file is the intended write. | line_budget: n/a — no skills/** diff.", "tests": "CENSUS RED LEG (round-1 census file driven against origin/main's rest-server.ts via untracked temporary copies, deleted after; git status clean): 195 cases, 29 red = 26 kept cells + 3 edge pins. Kept red cells: ?layers=true doc/runbook non-reader and anonymous, doc/intro anonymous, book non-reader and anonymous; /layers doc and book non-reader; /published doc, book, app crm/payroll/launchpad non-reader, dashboard reader and non-reader, object non-reader; /diff doc, book, object non-reader; /history and /audit doc, book, app payroll and launchpad non-reader. All 18 PENDING-DECISION #20156 cells PASSED on the pre-PR code, as they must, since they pin its answer. CENSUS GREEN (at 92e6b2fca and again at 8d314ac67 inside the full suite): 195/195. ABLATION (fix committed first): scripts/ablation-replace.mjs replaced the layered view's judge(document) call with an ungated serve. Anchor x1 -> x0, blob e51e675e -> 94176d58. 8 red: the 7 kept /layers + ?layers=true cells plus the per-layer edge pin. Pending cells stayed green. Restore: blob e51e675e == HEAD blob, git diff HEAD empty. rest-server.ts at 8d314ac67 is that same blob e51e675e. The subject is imported relatively from src, so no dist was involved. GATES (command -> exit; union at 8d314ac67): node scripts/check-adr-0087-registration.mjs --base origin/main -> 0 ;; node scripts/check-adr-0087-registration.mjs --self-test -> 0 ;; node scripts/check-changeset-no-major.mjs --base origin/main -> 0 ;; node scripts/check-changeset-no-major.mjs --self-test -> 0 ;; node scripts/check-ci-filter-parity.mjs -> 0 ;; node scripts/check-closing-keyword-parity.mjs -> 0 ;; node scripts/check-closing-keyword-parity.mjs --self-test -> 0 ;; node scripts/check-comment-mask-adoption.mjs -> 0 ;; node scripts/check-comment-mask-adoption.mjs --self-test -> 0 ;; node scripts/check-comment-mask-corpus.mjs -> 0 ;; node scripts/check-doc-frontmatter.mjs -> 0 ;; node scripts/check-doc-frontmatter.mjs --self-test -> 0 ;; node scripts/check-doc-route-spelling.mjs --advisory -> 0 ;; node scripts/check-doc-route-spelling.mjs --self-test -> 0 ;; node scripts/check-docs-section-name.mjs -> 0 ;; node scripts/check-docs-section-name.mjs --self-test -> 0 ;; node scripts/check-empty-changeset.mjs --base origin/main -> 0 ;; node scripts/check-empty-changeset.mjs --self-test -> 0 ;; node scripts/check-keyed-text-bounds.mjs -> 0 ;; node scripts/check-keyed-text-bounds.mjs --self-test -> 0 ;; node scripts/check-platform-object-tenancy-census.mjs -> 0 ;; node scripts/check-platform-object-tenancy-census.mjs --self-test -> 0 ;; node scripts/check-plugin-teardown-shape.mjs -> 0 ;; node scripts/check-plugin-teardown-shape.mjs --self-test -> 0 ;; node scripts/check-registry-log-declared.mjs -> 0 ;; node scripts/check-registry-log-declared.mjs --self-test -> 0 ;; node scripts/check-rest-log-spy-declared.mjs -> 0 ;; node scripts/check-rest-log-spy-declared.mjs --self-test -> 0 ;; node scripts/check-section-landing-index.mjs -> 0 ;; node scripts/check-section-landing-index.mjs --self-test -> 0 ;; node scripts/check-system-context-census.mjs -> 0 ;; node scripts/check-system-context-census.mjs --self-test -> 0 ;; node scripts/check-undeclared-dep-imports.mjs -> 0 ;; node scripts/check-undeclared-dep-imports.mjs --self-test -> 0 ;; node scripts/docs-audit/check-affected-docs.mjs -> 0 ;; node scripts/docs-audit/check-drift-comment.mjs -> 0 ;; node scripts/pm/release-rehearsal-clone.mjs --self-test -> 0 ;; pnpm --filter @objectstack/lint run check:doc-formula-expressions -> 0 ;; pnpm --filter @objectstack/lint run check:doc-security-posture -> 0 ;; pnpm --filter @objectstack/spec run check:docs -> 0 ;; pnpm --filter @objectstack/spec run check:duration-unit-keys -> 0 ;; pnpm --filter @objectstack/spec run check:empty-state -> 0 ;; pnpm --filter @objectstack/spec run check:liveness -> 0 ;; pnpm --filter @objectstack/spec run check:skill-examples -> 0 ;; pnpm --filter @objectstack/spec run check:strictness-ledger -> 0 ;; pnpm --filter @objectstack/spec run check:variant-docs -> 0 ;; pnpm --filter @objectstack/spec run check:yaml-examples -> 0 ;; pnpm check:authz-resolver -> 0 ;; pnpm check:changeset-gate-self-tests -> 0 ;; pnpm check:corpus-claim-drift -> 0 ;; pnpm check:cross-package-test-inputs -> 0 ;; pnpm check:dispatcher-error-vocabulary -> 0 ;; pnpm check:doc-anchors -> 0 ;; pnpm check:doc-authoring -> 0 ;; pnpm check:docs-audit-scope -> 0 ;; pnpm check:docs-redirects -> 0 ;; pnpm check:docs-single-h1 -> 0 ;; pnpm check:docs-spec-enumerations -> 0 ;; pnpm check:docs-transcript-drift -> 0 ;; pnpm check:driver-memory-census -> 0 ;; pnpm check:dts-closure -> 0 ;; pnpm check:dual-build-cjs-loads -> 3 = NOT MEASURED (PREREQUISITE NOT MET: needs the full package farm built; CI builds it) ;; pnpm check:engine-double-contract -> 0 ;; pnpm check:gitlink-declared -> 0 ;; pnpm check:issue-citations -> 0 ;; pnpm check:lean-entry-closure -> 0 ;; pnpm check:logger-receiver-detach -> 0 ;; pnpm check:nul-bytes -> 0 ;; pnpm check:objectql-double-limit -> 0 ;; pnpm check:objectui-changeset -> 0 ;; pnpm check:org-identifier -> 0 ;; pnpm check:page-declaration-shape -> 0 ;; pnpm check:pm-changeset-deadline-census -> 0 ;; pnpm check:published-files -> 0 ;; pnpm check:published-readme-links -> 0 ;; pnpm check:query-options-erasure -> 0 ;; pnpm check:react-page-adapter-contract -> 0 ;; pnpm check:refd-timer-probe -> 0 ;; pnpm check:role-word -> 0 ;; pnpm check:route-envelope -> 0 ;; pnpm check:skill-identifier-liveness -> 0 ;; pnpm check:slot-lookup -> 0 ;; pnpm check:sourcemap-no-sources-content -> 0 ;; pnpm check:test-source-alias -> 0 ;; pnpm check:tier-file-adoption -> 0 ;; pnpm check:type-check-coverage -> 0 ;; pnpm check:type-check-debt -> 3 = NOT MEASURED (PREREQUISITE NOT MET: needs the full package farm built; CI builds it) ;; pnpm check:vendor-version-stamps -> 0 ;; pnpm check:watch-hint-literal -> 0 ;; pnpm check:where-matcher -> 0 ;; node scripts/check-issue-citations.mjs -> 0 (diff-scoped BOARD check, read locally with the container token: 32 citations judged, 31 resolve, 1 cross-repo-unjudged, 0 findings; before this round the same run reported exit 2 with #10340 and #6241 allocated-but-absent) ;; pnpm lint -> 0 (repo-wide eslint . --no-inline-config, 114s) ;; pnpm --filter @objectstack/rest typecheck -> 0 (tsc --noEmit + check:test-typecheck) ;; pnpm --filter @objectstack/rest exec vitest run --project local -> 0 (198 files: 3534 passed, 1 skipped, 0 failed; census file 195/195) ;; node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-r1.list -> 0 (90 derived, 88 run, 2 NOT-MEASURED, 0 UNRUN)", "mcp_calls": "0 this round. Round 0: 1 (mcp__Claude_Code_Remote__add_repo, objectstack-ai/objectui read, refused by the permission classifier). 0 GitHub MCP calls, 0 MCP write tools, across both rounds.", "api_writes": "1 this round — POST /repos/objectstack-ai/objectstack/dispatches (the fleet-write relay, executed as objectstack-fleet[bot]) -> POST /repos/objectstack-ai/objectstack/issues/20156/comments (this os-dev-report). Plus git push of the branch (not REST): a733beb2e, 92e6b2fca, 8d314ac67. No PR body PATCH, no label write, no claim.", "open_questions": [ { "question": "Seat or maintainer, on the card: what should the app row of the stored-version doors (/layers, ?layers=true, /diff) answer? These doors are unchanged in this PR and pinned as PENDING-DECISION #20156.", "options": [ "A — refuse a caller the plain read would serve only part of an app (403 PERMISSION_DENIED). Measured by the review: this locks out platform admins who lack an entry permission.", "B — writer exemption in ADR-0106 D4's shape: callers that metaWriteCapabilityVerdict admits see the stored app whole; everyone else gets the plain read's answer (pruned or refused). This is a new permission boundary.", "C — serve pruned plus a write-side guard that refuses a PUT dropping entries withheld from the writer. This is a write-door change and a new refusal." ], "recommendation": "B, on a maintainer ruling. Round 0 recommended A; the review measured that A locks out platform admins, so that recommendation is withdrawn. Business need: the documented designer consumer (DesignerEditorWrapper via ?layers=true) must see the whole stored app to save it without loss, and members who are not authors have no need of the authoring views. Long-term: B carries ADR-0106's 'whoever may write sees all of it' invariant, already ruled for object schemas, to apps, so there is one rule for both. AI-safety: an AI author gets the whole stored app and cannot silently delete what it cannot see; a non-writer gets the plain read's answer. Startup scope: B reuses metaWriteCapabilityVerdict, adds no new gate, and touches one arm. A fails the admin lockout; C adds write-side machinery." } ], "out_of_scope_findings": [ "Round 0's runtime-dispatcher finding is filed by the seat as #20193 — #20193 remains open; nothing new filed this round.", "carrier: 承接者:无 · noted, not filed — GET /meta/:type/:name/references serves the type/name/label of OTHER items that reference this one, applying none of those items' own gates. Not measured, outside this card's class, declared exempt in the census; PR Acceptance notes only.", "carrier: 承接者:无 · noted, not filed — the route-table census catches only routes spelled /meta/:type/:name/...; literal-typed doors (/meta/book/:name/tree, /meta/object/:name/state/:field) fall outside it. Both existing ones are outside the class, and /book/:name/tree has its own section 6.7 gate. The review's note is carried into the PR body's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
REWORK (patch round 2): PR #20190 at
8d314ac6, the pending-decision exemption narrowed to the partial cellsdomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review verdict, written 2026-09-27T08:53Z8d314ac6: green. 38successand 4 skips that are rostered expected skips. The earlier red citation step is resolved.5854378054on the PR). The seat verified it at source and upholds it. The seat also rules the dev's deviation (a) in round 1: the seat did not mean the whole app row.What round 1 got right (the review judged each correct, with evidence):
filterAppForUserWithReasonis byte-identical to base);dashboardchangeset sentence is corrected;What exceeds REWORK
5853865127:requiredPermissionsthe caller lacks;8d314ac6the six non-reader cells (payrollandlaunchpad×/layers,?layers=true,/diff) serve that app whole, including an unpublished app to a signed-in member./historyand/auditof the same app refuse.origin/main:ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissionsholds bothstudio.accessandsetup.access, and the unpublished branch offilterAppForUserWithReasonrefuses only a caller with neither./layersback to that answer is the same pull-back the REWORK kept.Required changes (round 2):
app: 'pending-decision', still run the plain read's app gate. Where the plain read refuses the app whole, answer its refusal (403 PERMISSION_DENIED, or the absence answer). Otherwise serve the stored app unpruned, as pre-PR./diffmust then fetch the current document forapp. In the census, the pending mark holds only on the pruned cells; thepayrollandlaunchpadnon-reader cells return to the plain read's envelope with no secret in the body, as atace7e1df.approw of/layers,?layers=trueand/diff");content/docs/ui/apps.mdx(about :358-368): the same precision.The decision the card moves to after landing is unchanged in scope: the partial cells, options A / B / C. Patch round to the same dev, on the same claim and branch.
Generated by Claude Code
18 remaining items
objectstack-fleet commentedon Sep 27, 2026
Landed: PR #20284 →
2dccb7d494ffc9190b7df36b4d91e94e9610a28adomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· landing record, written 2026-09-27T19:21Zgit rev-list --parents -n 1gives 2 fields.2dccb7d4:author-exempthas 5 hits inpackages/rest/src/meta-item-read-gate.ts;metaSaveVerdicthas 6 hits inpackages/rest/src/rest-server.ts;PENDING_DECISIONhas 0 hits in the census;.changeset/20156-app-author-exemption.mdis in the tree.e6b7d8c8: 0 / 0 / 11, and the changeset is absent, so the instrument can fire.completedby the PR'sFixes, which executes ruling5856774816(letter B) in full.pm:dispatchedand the assignee were cleared in this act.rest-server.ts/ gate-module fence next, 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 (p2) follows it.GET /meta/app/:name?state=draft), and the designers merge it over the whole stored app, so a draft save drops the navigation entries withheld from that author #20290 (filed from this round: an app author's draft baseline is read through the pruned plain read) is bare, awaiting triage.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Ruling B here, item 2, narrowed for two doors by ruling B on #20378 (batch #234 item 2,
5865708652) · 2026-09-28T07:52ZDirector seat (objectstack#12708, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3). Record only; nothing else on this card moves./diffand/historybecome authoring doors: a callermayReadPendingDraftsdoes not admit is refused 403 as/meta/_draftsrefuses, before any read. The premise this ruling rested on — that a stored version can be pruned per audience like a plain read — does not hold for those two doors:sys_metadata_historycannot tell a draft save from an active save, so the pruned published-only answer does not exist without a schema change the maintainer did not take./layersand?layers=trueare unchanged: they read the active row only and keep item 2's pruned plain-read answer as ruled here.objectstack-fleet commentedon Sep 28, 2026
Ruling B (
5856774816) narrowed for/diffand/history: per ruling5865708652on #20378 (letter B, the maintainer's 「同意」 in the director seat's batch #234, 2026-09-28),GET /meta/:type/:name/diffandGET /meta/:type/:name/historyare authoring doors. A callermayReadPendingDraftsdoes not admit is refused asGET /meta/_draftsrefuses, because the history store cannot tell a draft save from an active save./layersand?layers=truekeep this ruling's pruned plain-read answer. Posted by thedomain:cliseat (#6024,session_01UYBdGBzWSrAMzpW8ah3GbP) as that ruling's execution parameter directs: 「a one-line note under ruling B on #20156 (comment there, not a body rewrite) records the narrowing for the two doors」.Generated by Claude Code
test(rest): pin /diff and /history as authoring doors; docs and chang…
fix(rest): one per-caller read gate for GET /meta/:type/:name and eve…
fix(runtime,rest): the dispatcher /meta item reads ask the per-caller…
fix(rest): the stored-version doors serve an app whole to whoever may…
fix(rest): the draft read serves an app whole to whoever may save it,…
fix(rest, runtime): serve pending metadata drafts only to a caller wi…
fix(rest): /meta/:type/:name/diff and /history are authoring doors, r…
fix(rest): /meta/:type/:name/audit is an authoring door, refused as /…
fix(rest, runtime): the dispatcher serves the layered view on both sp…