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
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 2 (rework on delta review 5854378054 and seat verdict 5854384500). New head: 8116184fe60c57d304ef275f925da03c78ea9282 (pushed; remote head verified equal). (1) Under app: 'pending-decision', metaItemReadGate now still runs the plain read's app gate (filterAppForUserWithReason, unchanged and byte-identical to base). An app refused WHOLE (app-level requiredPermissions -> 403 PERMISSION_DENIED; unpublished to a non-builder -> the absence answer) is refused exactly as the plain read refuses it. Otherwise the stored document is served UNPRUNED, as pre-PR. gatesPerCaller now answers true for app on every policy, so /diff fetches and judges the current document (and both sides) for app again. Fault paths are unchanged: resolveExecCtx faults rethrow the authz-store outage, and getMetaItem faults propagate to handleRouteError. The docs-audience entry arm is not resolved under pending-decision, because it only prunes entries and that arm serves the stored app unpruned, so it adds no read and no fault path. (2) Census: the PENDING-DECISION #20156 mark holds only on the pruned cells (app/crm non-reader x /layers, ?layers=true, /diff: 3 cells). The payroll and launchpad non-reader cells on those doors are back to envelope(res) === envelope(plain) with no secret in the body, as at ace7e1df. The scope test was hardened: PENDING_DECISION.answer === 'pruned', the doors are pinned literally (the review's minor note), and a second test enumerates the exempt cells and asserts there is no whole-refusal cell among them. Measured: widening the predicate to 'refused' turns that test red. (3) Changeset: states exactly which callers the three doors still serve a stored app to (only a caller the plain read serves part of the app, never one it refuses whole); the closing sentence now reads '... now receives the plain read's answer, except the app row of /layers, ?layers=true and /diff for a caller the plain read serves only part of the app'; and /diff's 'before' list names an app's navigation again. (4) content/docs/ui/apps.mdx (about :358-370): every door refuses an app the caller may not open (403 or 404), and the stored-version doors do not yet prune the entries of an app the caller may open, pending a decision. (5) The PR body was NOT edited. The full replacement is saved at /tmp/claude-0/-home-user-objectstack/df3d1045-8eb6-5b68-98d9-141980edea33/scratchpad/issue-20156/pr-body-r2.md, first line 'Part of #20156'. origin/main was merged once, before pushing (63ba41eb5, 5 upstream commits, none on these paths). | new_head: 8116184fe60c57d304ef275f925da03c78ea9282. Round-2 commits: 91b49f1d5 (fix), 63ba41eb5 (merge origin/main at 1c8b320a8), 8116184fe (test). | files_changed (vs merge base 1c8b320a8): packages/rest/src/rest-server.ts; packages/rest/src/meta-alternate-door-read-gates.test.ts; packages/rest/src/execctx-consumer-census.test.ts (unchanged this round); content/docs/ui/apps.mdx; .changeset/20156-alternate-door-read-gates.md. 5 files, +1302/-172. | deviations (round 2): (a) The seat's ruling on round-1 deviation (a) is applied, and that deviation is withdrawn. (b) Under pending-decision the docs-audience entry gate is deliberately not resolved; this is not an arm change, because the plain read's whole-app refusal does not depend on it. (c) A mid-round draft of the pending /diff cell asserted the current document WAS read. That made the cell fail on the pre-PR code, so it was removed in 8116184fe: pending cells pin the pre-PR ANSWER only, and the whole-refusal cells prove the gate runs. (d) The fourth gate batch hit the 590s foreground cap, cutting off pnpm check:type-check-debt. That gate was re-run alone and recorded exit 3, PREREQUISITE NOT MET, as in every round. (e) Round-0 deviations 1, 3, 5, 6 and 7 stand. Round-0 deviation 2 and round-1 deviation (a) are withdrawn. | pr_body_file: /tmp/claude-0/-home-user-objectstack/df3d1045-8eb6-5b68-98d9-141980edea33/scratchpad/issue-20156/pr-body-r2.md (10243 bytes). It was built from the live body, which was verified byte-identical to round 1's saved body. It is lint-clean: no angle brackets, no closing keyword near a number, no control bytes, no model name. The diff against the live body is at /tmp/claude-0/-home-user-objectstack/df3d1045-8eb6-5b68-98d9-141980edea33/scratchpad/issue-20156/pr-body-r2.diff (74 lines). | line_budget: n/a — no skills/** diff.", "tests": "CENSUS RED LEG (census at 8116184fe driven against origin/main's rest-server.ts via untracked temporary copies, deleted after; git status clean): 196 cases, 35 red = 32 kept cells + 3 edge pins. By door: ?layers=true 7, /layers 4, /published 8, /diff 5, /history 4, /audit 4. The 6 cells new this round are payroll and launchpad non-reader x ?layers=true, /layers, /diff. The 3 PENDING-DECISION cells PASSED on the pre-PR code, as they must. Both scope tests passed. CENSUS GREEN at 8116184fe: 196/196. ABLATION 1, the narrowed arm (fix committed first): scripts/ablation-replace.mjs changed 'if (!gateInputs) return serve(document);' to also serve under pending-decision, which is round 1's behaviour. Anchor x1 -> x0, blob 6783daa1 -> 970dc972. Whole census: 196 run, 6 red, exactly payroll and launchpad non-reader x ?layers=true, /layers, /diff. Restore: blob 6783daa1 == HEAD, git diff HEAD empty. rest-server.ts at 8116184fe is blob 6783daa1. ABLATION 2, exemption scope honesty: in the census, isPendingDecision was widened to also match want.kind === 'refused'. Test blob b1aaa717 -> d21265ab. The scope test 'the cells it yields are exactly the partial app cells' FAILED, and the literal-door test passed. Restore: blob b1aaa717 == HEAD, git diff HEAD empty. Subjects are imported relatively from src, so no dist was involved. GATES (command -> exit; union at 8116184fe): 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 --base origin/main -> 0 (the REAL diff-scoped board check, read locally with the container token: 32 citations judged, 31 resolve, 1 cross-repo-unjudged, 0 findings) ;; pnpm lint -> 0 (repo-wide eslint . --no-inline-config, 121s) ;; pnpm --filter @objectstack/rest typecheck -> 0 (tsc --noEmit + check:test-typecheck) ;; pnpm --filter @objectstack/rest exec vitest run --project local -> 0 (198 files: 3535 passed, 1 skipped, 0 failed; census file 196/196) ;; node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-r2.list -> 0 (90 derived from merge base 1c8b320a8, 88 run, 2 NOT-MEASURED, 0 UNRUN)", "mcp_calls": "0 this round. Across all rounds: 1 (round 0: mcp__Claude_Code_Remote__add_repo, objectstack-ai/objectui read, refused by the permission classifier). 0 GitHub MCP calls, 0 MCP write tools.", "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): 91b49f1d5, 63ba41eb5, 8116184fe. No PR body edit, no label write, no claim, no MCP write.", "open_questions": [ { "question": "Maintainer, on the card: what should /layers, ?layers=true and /diff answer a caller the plain read would serve only PART of an app? These are the 3 PENDING-DECISION #20156 cells; everything else on these doors now matches the plain read.", "options": [ "A — refuse (403 PERMISSION_DENIED). Measured by the review: this locks out platform admins who lack one entry's permission.", "B — writer exemption in ADR-0106 D4's shape: callers that metaWriteCapabilityVerdict admits see the stored app unpruned; everyone else gets the plain read's pruned app. This is a new permission boundary.", "C — serve pruned plus a write-side guard refusing a PUT that drops entries withheld from the writer. This is a write-door change and a new refusal." ], "recommendation": "B, on a maintainer ruling; unchanged from round 1. Business need: the documented designer consumer (DesignerEditorWrapper via ?layers=true) must load the whole stored app to save it without loss, and non-authors do not use authoring views. Long-term: this is ADR-0106's 'whoever may write sees all of it', already ruled for object schemas, applied to apps, so one rule covers both. AI-safety: an AI author cannot silently delete entries it cannot see. Startup scope: B reuses metaWriteCapabilityVerdict, adds no gate, and touches one branch of one arm." } ], "out_of_scope_findings": [ "Nothing new filed this round. The runtime-dispatcher finding is the seat's #20193 — #20193 remains open.", "carrier: 承接者:无 · noted, not filed — GET /meta/:type/:name/references serves identities of OTHER items without their 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 /meta/:type/:name/... spellings; literal-typed doors fall outside it (both existing ones are outside the class); PR Acceptance notes only." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
ACCEPT: PR #20190 at
8116184f, R3 of thedomain:cliseat (after patch rounds 1 and 2)domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review of record, written 2026-09-27T10:09ZReviewed against GitHub and
origin/main, not against the report.Checklist:
main. The first line isPart of #20156, and the body carries no closing keyword (Part-of PR must not also close its card: success). [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 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 are named asremains open. A line-initialClause-②: nois present. The seat wrote the round-2 body from the dev's saved draft; it reads back byte-identical.check-governed-merges --pr 20190: NOT governed; 1474 changed lines, under 5000):packages/rest/src/rest-server.ts;content/docs/ui/apps.mdx;@objectstack/restpatchchangeset.All are inside the claimed surface. Nothing under
content/docs/releases/. The runtime dispatcher half is [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, untouched here.metaItemReadGateserves the plain read and every door beside it./publishedgets every arm; the ADR-0106 object mask applies on/publishedand/diff./historyand/auditrefuse where the plain read refuses whole./layers,?layers=trueand/diff, an app the plain read refuses whole is refused with the plain read's envelope, and only a caller the plain read serves part of an app still receives the stored app. That is the declared pending-decision row.main: 35 red.8116184f: 31 checkssuccessby latest name, includingTypeScript Type Check, allTest Coreshards andLint & Repo Gateswith its issue-citation step. 4 are skipped:Auto Label,Check PR Size,Console Pin GateandPacked-tarball smoke (opt-in), each a rostered expected skip inscripts/pm/check-expected-skips.mjs. TheTest Core/TypeScript Type Checkreds this PR showed were on superseded heads (92e6b2fc,63ba41eb): aggregator failures over lanescancelledby the next push, so not this head's verdict.content/docs, and a security boundary):5853855259): a red citation step, refuse-on-partial as a new boundary that locks out platform admins, and a falsedashboardsentence;5854378054): the exemption widened to the whole app row;5854917613).All three were at-tier independent agents given only the card, the rulings and the PR.
Decisions of record:
5853865127, upheld in5854384500): the p1 content leak lands now, and only the NEW refuse-on-partial boundary is held for the maintainer. The seat does not rule a new permission boundary.patch+Clause-②: no: upheld by all three reviews. Every arm pulls a door back to a gate the contract already declares (ADR-0046 §6.7, ADR-0106, ADR-0045 §3,apps.mdx), and no authorable surface is removed.Out-of-scope findings:
/metaitem reads with no per-caller gate → filed [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 (R3)./referencesserves other items' identities without their gates, and the route-table census covers only/meta/:type/:name/...spellings → PR Acceptance notes, no carrier. Unmeasured, and outside this card's class.requiresServicewhole-refusal is not applied on the non-/publisheddoors. It is a per-deployment gate (ADR-0057 D10), not per-caller, so it is outside this card's class and not a defect (review round 2 ③).After landing: this card stays open (
Part of). It moves toneeds-user-decisionfor the partialappcells, with options A / B / C and the reviews' measurements.Next: landing through the relay ops
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Landed: PR #20190 →
585c9af587a165c066c7815185a6652c2b9c545e(Part of: this card stays open)domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· landing record, written 2026-09-27T10:30Zgit rev-list --parents -n 1gives 2 fields.585c9af5:metaItemReadGatehas 14 hits inpackages/rest/src/rest-server.ts, andSTORED_VERSION_DOOR_POLICYhas 3. The censuspackages/rest/src/meta-alternate-door-read-gates.test.tsand.changeset/20156-alternate-door-read-gates.mdare in the tree.b09ce678. The instrument can fire.Part of #20156). The partialappcells of the stored-version doors move to the decision box in the next comment, with the same label write:pm:dispatched→needs-user-decision, assignee cleared.packages/rest/src/rest-server.tsfor [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 (with the declared rider from REWORK5853969393on [finding]packages/client/src/index.ts:2151JSDoc still documents404 [no_draft], a spelling the runtime no longer emits after #16245 #19704: the publish route comment404 [no_draft]) and for [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.Blocked-by: #20156): its triage note names the blocker as the shared gate "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." That gate is now onmain. The open decision here concerns only the three stored-version doors' partialappcells, and [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's four rows are plain-read and/publishedanswers. So [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 does not wait on it; the seat re-derives and releases it in its own comment.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
需要维护者裁决:设计器读 app 时,对「只能看到 app 一部分」的人该怎么答
domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· 决策卡落卡,written 2026-09-27T10:31Z。⛔ 席位不裁这个问题:它划的是一条新的权限边界。状态转换(本评论同笔):
pm:dispatched→needs-user-decision,assignee 清空。PR #20190 已按Part of #20156落地(合并585c9af5),本卡其余部分都已交付,只剩下面这一格等裁决。一句话问题
一个 app 里,有些导航条目是某些成员无权看的。成员打开 app 时,那些条目被隐藏了。可是 Studio 设计器读 app 用的是另外三个入口,那里目前把完整 app 原样返回给任何能打开这个 app 的人,于是被隐藏条目的名字在那里仍然看得见。要不要在这三个入口也隐藏?隐藏的话,设计器保存时会把看不见的条目删掉。
背景与已交付部分
/meta/:type/:name旁边的所有读入口都走普通读的同一个权限闸:/published对所有类型给出与普通读完全相同的答复;/published和/diff;requiredPermissions,或未发布给非构建者),在/layers、?layers=true、/diff、/history、/audit上都照样拒绝。/layers、?layers=true、/diff三个存储版本入口上,对普通读只给 app 一部分的调用者(能打开 app,但部分条目被requiredPermissions或文档受众隐藏),仍原样返回完整存储版本。PENDING-DECISION #20156,范围测试保证它不会扩大。Governing text:
isSystem与平台管理员豁免,「Studio/Setup authoring requires the full schema… The exemption is a caller property, not a route property」。这是对象的先例,没有覆盖 app。content/docs/ui/apps.mdx的 app 级requiredPermissions在服务端检查。content/docs/plugins/adding-a-metadata-type.mdx:Studio 的DesignerEditorWrapper用?layers=true读入,再把读到的内容存回去。协议声明: spec 没有声明设计器入口对部分可见调用者的答复。这是新边界,不是缺陷修复,所以不改协议,由维护者定方向。
Prior rulings read: layers,designer,requiredpermissions,authoring,exempt,prune,withheld → 72 hits。命中的有 ADR-0018 Decision §4、ADR-0031 Decision §6/§7、ADR-0033 Decision §1、ADR-0068 D4、ADR-0106 D4、ADR-0005 Decision §1、ADR-0007 Decision §1。thread: none。
带复核命令的前提:
git grep -n "PENDING-DECISION #20156" origin/main -- packages/rest/src/meta-alternate-door-read-gates.test.ts(预期有命中)。git show origin/main:packages/spec/src/identity/eval-user.zod.ts | awk '/ADMIN_FULL_ACCESS_CAPABILITIES/,/^};/'(systemPermissions共 7 项,不含任何业务权限)。git grep -n "reqApp.every" origin/main -- packages/rest/src/rest-server.ts。选项 × 真实代价
403metaWriteCapabilityVerdict放行者)看到完整存储版本;其他人拿到普通读的裁剪版本业务含义直译
四轴分析(业务立场)
DesignerEditorWrapper走?layers=true,已在文档中实测到)。普通成员的界面不调用这三个入口,所以隐藏条目只影响直接调 API 的成员。今天的暴露只是条目名,没有条目内容,分诊定为 p2。metaWriteCapabilityVerdict,只改一个分支,不新增门禁,也不新增错误码。C 要新造写侧比对机制和一个新的拒绝。A 代码最少,但会产生管理员被挡的支持成本。D 零成本,但留着暴露。os-decision-facets
?layers=true读入后存回),必须拿到完整 app;普通成员界面不走这三个入口。Prior rulings read: layers,designer,requiredpermissions,authoring,exempt,prune,withheld → 72 hits; ADR-0018 Decision §4, ADR-0031 Decision §6, ADR-0031 Decision §7, ADR-0033 Decision §1, ADR-0068 D4, ADR-0106 D4, ADR-0005 Decision §1, ADR-0007 Decision §1; thread: none; repo: objectstack-ai/objectstack
推荐:B(作者豁免,按 ADR-0106 D4 的形状)。回退:D(维持待裁状态,暴露留在 p2),不推荐 A 或 C 作为回退。自检:只看①选 B;②③④ 是否翻转:否。
置信缺口:
metaWriteCapabilityVerdict(谁能写这个 app),还是 ADR-0106 D4 字面的「平台管理员」判定,是一个执行参数,请一并裁定。席位推荐前者,理由是能存回的人必须看全,否则保存会丢条目。裁后执行
metaItemReadGate的pending-decision分支按裁定的判定放行作者,其余调用者给普通读的裁剪版本。patch。Fixes关闭。Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Ruling: batch #227 item 1 · letter B · maintainer 「开始总监决裁」 2026-09-27T14:35Z
Director seat, summon #30 续,
session_01AsCNgFBs8HCjwhyHQsFbx3. Batch #227 was presented in the seat chat with this seat's recommendation on each item (thedomain:cliseat's analysis 5855061934 re-read, four axes not flipped); the maintainer answered 「开始总监决裁」, and the batch is ruled as presented. Maintainer verbatim recorded here per the charter.Ruled: B — author exemption, in ADR-0106 D4's shape. On the three stored-version doors (
/layers,?layers=true,/diff) a caller who may write the app reads the full stored version; every other caller who may open the app reads exactly what the plain read gives them, pruned.Readings on
origin/mainab820016b3: the censuspackages/rest/src/meta-alternate-door-read-gates.test.ts:302-329declares the three partial-appcells asPENDING_DECISIONwith anchor#20156;metaWriteCapabilityVerdictis imported atrest-server.ts:69and is the existing 「who may write this item」 verdict; ADR-0106 D4 already rules the same shape for object schemas (「the exemption is a caller property, not a route property」); ADR-0045 §3 keeps unpublished apps invisible to non-builders, unchanged by this ruling. Prior-ruling sweep overAGENTS.md,docs/adr,packages/spec/srcfor the designer-door / caller-property terms: ADR-0106 D4 is the only hit, and it does not coverapp.Mainstream reading the ruling rests on: Salesforce's Lightning App Builder shows an administrator who can edit the app every navigation item, while end users see only the items their profile grants; ServiceNow's application designers read the full application definition regardless of end-user visibility rules. Read-to-display is pruned per user; read-to-edit is whole for the editor. B extends the rule ADR-0106 D4 already set for objects to apps, so the platform keeps one rule rather than one for objects and another for apps. A (refuse) blocks an administrator who lacks one item's permission from the designer entirely, C (prune plus a write-side guard) makes the editor save a document they cannot see whole, D leaves the p2 exposure.
Execution parameters, ruled here so the dispatch needs no second decision:
metaWriteCapabilityVerdictfor the app (whoever can save it must see it whole, or a save drops entries silently) — ⛔ not the literal 「platform administrator」 reading; the seat's recommendation is adopted.PENDING_DECISIONcells become ruled answers (author: full; non-author: pruned) plus an author-control cell; the exemption constant and its docblock retire with this ruling's citation.patchfor@objectstack/rest;Clause-②: no(no published accept set moves; the doors' answers narrow to the plain read's for non-authors, a permission fix).domain:cliseat dispatches the follow-up PR as its analysis says; the card closes byFixeswith it.Confidence gap carried: whether any objectui surface other than Studio's
DesignerEditorWrapperreads these doors is unmeasured; the dev measures it first and reports a second reader rather than tolerating it.State:
needs-user-decision→pm:queuein this act (domain:cli· p1 ·securityunchanged); body first line set to this ruling.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
⏸️ Hold — the ruling above is held for the maintainer's fuller review of batch #227. Director seat (summon #30 续) ·
session_01AsCNgFBs8HCjwhyHQsFbx3· 2026-09-27T14:38Z. The maintainer's word after the batch was posted: 「227 你写的太简单了」. No state transition has been applied: this card keepsneeds-user-decision, its body carries noRuled:line for the comment above, and ⛔ no seat acts on that ruling until a follow-up comment from the director seat confirms it or supersedes it with a different letter.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
✅ Confirmed — the ruling above (5856774816, letter B) is in force. Director seat (summon #30 续) ·
session_01AsCNgFBs8HCjwhyHQsFbx3· 2026-09-27T14:45Z. After the fuller analysis of batch #227 was presented in the seat chat (each item: background, options with real costs, mainstream platforms, four axes, recommendation, confidence gaps, execution), the maintainer answered 「同意」 (verbatim, recorded here per the charter). The hold at 5856800585 is lifted. State transition applied in this act:needs-user-decision→pm:queue(domain:cli· p1 ·securityunchanged); the body's first line now carriesRuled: 5856774816 · letter B. Execution follows the 裁后执行 section of the ruling.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 3, eleventh slot (batch 3; executing ruling 5856774816, letter B; a slot freed when PR #20236 landed; beside #20242 and #19098 on disjoint file surfaces)
Session:
session_01UYBdGBzWSrAMzpW8ah3GbPAccount:
os-litantBranch:
claude/issue-20156-app-author-exemptionWorktree:
objectstack-issue-20156Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/rest/src/meta-item-read-gate.ts(MetaReadGatePolicy.app'spending-decisionarm becomes the ruled author exemption, plus whatever caller or source field carries the write verdict),packages/rest/src/rest-server.ts(STORED_VERSION_DOOR_POLICYand the three stored-version doors that supply that verdict:/layers,?layers=,/diff),packages/rest/src/meta-alternate-door-read-gates.test.ts(the census:PENDING_DECISIONretires; ruled cells plus an author control),packages/rest/src/execctx-consumer-census.test.tsonly if a site count moves,packages/runtime/src/domains/meta.tsand its parity test only if the shared gate's signature forces the runtime caller to adapt,packages/spec/liveness/*.jsonanchor lines only if a moved symbol breaks one, the pending.changeset/20156-alternate-door-read-gates.md(its 「unchanged in this release … pending a decision」 paragraph, corrected in place), and one new.changeset/20156-*.md. Added in place after the dev report:content/docs/ui/apps.mdx(its sentence that the stored-version doors do not yet prune, pending a decision, is now false) andcontent/docs/permissions/system-context.mdx(row 49 re-anchored tometaSaveVerdict, becausecheck:system-context-censuscounts the movedisSystemread site).packages/metadata-core/**(metaWriteCapabilityVerdict,domain:engine) is read-only, and so is the rest ofpackages/spec/**. Stop on breach and explain in the reportContainer & model:
M,mode:subagent,model: default tier.dispatch-gates.mjs --tiernames no path-derived mandate for this surface (「the surface hits none of the 3 declared glob(s)」).Clause-②: no
Thread-read: 5856866273
Serial constraints cleared:
#20193 (PR #20236) released rest-server.ts, meta-item-read-gate.ts and runtime meta.ts at 2bcd5cfe (landing record 5857383747). No open PR touches this surface: the file lists of all 9 open PRs were read in this act. #20247 and #20246 touch other packages/rest files (analytics-filter-refusal-envelope.test.ts; import-*.ts), which are disjoint. #20237 (p1) and #20139 (p2) are queued on the same fence and wait behind this card (unlock 5857389777). R3's other in-flight slots are #20242 (a packages/client test file) and #19098 (packages/cli generate.ts, content/docs/api/data-flow.mdx), both disjointTaken 2026-09-27T15:53Z,
origin/mainafter2bcd5cfe. Executes ruling5856774816(batch #227 item 1, letter B, confirmed5856866273, maintainer 「同意」). On/layers,?layers=trueand/diff, a caller who may write the app reads the full stored version, and every other caller reads the plain read's pruned answer.Clause-②: noandpatch, per ruling item 4. The card closes byFixes(item 5). The earlier branchclaude/issue-20156-alternate-door-read-gateslanded as PR #20190 (Part of) and is not reused. 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-app-author-exemption", "pr": "https://github.com/objectstack-ai/objectstack/pull/20284", "session": "session_01UYBdGBzWSrAMzpW8ah3GbP — this run's harness-stamped id (each commit's Claude-Session trailer; subagent = parent's)", "premise_still_valid": true, "summary": "Executed ruling 5856774816 (letter B, confirmed 5856866273). On /layers, ?layers=true and /diff, a caller who may save the app reads the full stored version, and every other caller who may open it reads it pruned. The plain read, /published, /history, /audit, every whole-refusal cell and the doc, book and dashboard arms are unchanged. The ruled cells were measured on the real handlers.\n(1) Gate: MetaReadGatePolicy.app is now gate or author-exempt, and pending-decision is retired. The new optional MetaReadGateCaller.mayWriteItem carries the exemption as a CALLER property (ADR-0106 D4), and the policy says only whether a door honours it.\n(2) Transport: RestServer.metaSaveVerdict is the save door's admission spelled once. PUT /meta/:type/:name calls it, and the stored-version doors attach it to the caller as mayWriteItem. STORED_VERSION_DOOR_POLICY is per-caller with app author-exempt.\n(3) MECHANISM FIX the order did not anticipate: the layered view and /diff discarded a serve verdict's document and acted only on refusals, so flipping the policy alone would have pruned nothing. The layered view now replaces each layer with the gate's document, after judging every layer. /diff emits each bucket's values from the judged sides and neither adds nor drops an entry. That follows the redaction precedent (ruling 5299845282 option B) and the ADR-0106 mask beside it.\n(4) Census: PENDING_DECISION and its docblock are retired. AUTHOR_EXEMPTION cites the ruling. There is a new author caller (manage_metadata, without finance.access or crm_admin), and three scope pins: exactly the three stored-version doors; exactly the three author partial crm cells change, while six author whole refusals stay refused; and savesApps equals the real PUT /meta/app/crm answer for each caller. Two edge pins cover the non-author /layers on a book-read fault and an org-presentation author, who is refused the app save, so the app is pruned for them.\n(5) Pending changeset .changeset/20156-alternate-door-read-gates.md is corrected in place, plus a new .changeset/20156-app-author-exemption.md (@objectstack/rest patch). content/docs/ui/apps.mdx is corrected, and content/docs/permissions/system-context.mdx row 49 now anchors metaSaveVerdict, with the count regenerated by the gate's own regenerate flag.\nPR #20284 is a draft on head 9d955a369. Its first line is the Fixes keyword for this card, followed by a bare Clause-②: no. #20237 and #20139 are named only as remains open. The body reads back byte-identical (14340 bytes). CI at report time: 32 check runs, 11 success, 2 skipped, 18 in_progress and 1 failure. The failure is Check Changeset, red solely on the declared deliberate correction of the pending note (see open_questions).\nfiles_changed (vs merge base a9fb83ef0, 7 files, +462/-178): packages/rest/src/meta-item-read-gate.ts; packages/rest/src/rest-server.ts; packages/rest/src/meta-alternate-door-read-gates.test.ts; .changeset/20156-alternate-door-read-gates.md (edited); .changeset/20156-app-author-exemption.md (new); content/docs/ui/apps.mdx; content/docs/permissions/system-context.mdx. No runtime, metadata-core or spec file.\ndeviations:\n(a) The file surface was amended beyond the claim, twice, because this change makes published text false or red. content/docs/ui/apps.mdx said the stored-version doors do not yet prune, pending a decision. content/docs/permissions/system-context.mdx was needed because check:system-context-census went red: the save door's isSystem read moved into metaSaveVerdict. The seat should add both to the claim's surface.\n(b) Zone 3 route: the transport attaches mayWriteItem on every door whose policy honours the exemption, gated on the policy, NOT on type app. Measured: it is a pure verdict over the already-memoised exec ctx and the static type registry, so no door pays a read. Keeping the type decision in the gate follows the module's own rule of no decision in a transport adapter.\n(c) Mechanism assumption 1 was partially falsified, as described in (3) above.\n(d) The ruling's known reader, DesignerEditorWrapper, is absent from objectui source at the pin (see tests: objectui census).\n(e) origin/main moved 6 commits (to 89f87f234) after this branch's merge. They touch none of these files, and only two gate inputs changed upstream: a doc-authoring baseline shrink for another file, and the error-status script. Not re-merged; CI's merge ref covers them.\n(f) The full rest suite was measured at 6780cd777, and the runtime typecheck and parity at bad8baa63. The packages/ delta since then is one docblock line (git diff 6780cd777 9d955a369 -- packages/).\n(g) Commit trailers are the model-free AGENTS.md pair. A harness reminder asked for a model-named Co-Authored-By; it was not used, because AGENTS.md takes precedence.\nZone 2 readings:\nitem 1: the site is confirmed by symbol at e46218674, and there is no runtime change.\nitem 2: the save door's full caller admission is metaWriteCapabilityVerdict alone. registerPerItemRoute adds only the endpoints.item switch, and saveMetaItem's refusals judge the item and the scope, never the caller. app is allowOrgOverride false, so only isSystem or manage_metadata admit. One shared spelling (metaSaveVerdict) was possible, so needs_decision did not apply.\nitem 3: the exemption is carried on the caller, and there is no route test in the gate.\nitem 4: the types are unreleased. The published tarball was measured (see tests), and .changeset/20193-dispatcher-meta-read-gate.md is present on origin/main 89f87f234.\nitem 5: done. execctx-consumer-census.test.ts is unchanged and green: no new resolveExecCtx site, because the verdict is derived inside the existing resolveCaller adapter.\nitem 6: the note is corrected in place and has not been consumed. The new changeset is required because Check Changeset counts only ADDED files (--diff-filter=A).\nitem 7: measured first (see tests).\nline_budget: n/a — no skills/** diff.\ncleanup: the worktree node_modules were removed and the worktree removed after the PR opened (git worktree list shows no objectstack-issue-20156). No lock ticket or background job remains.", "tests": "Anchors: the final HEAD is 9d955a369. Lock seconds are shared-box figures.\nCENSUS packages/rest/src/meta-alternate-door-read-gates.test.ts: 261/261 at 9d955a369 (vitest json reporter). That is 1 route-table test, 36 plain-read rows (9 subjects x 4 callers), 3 author-exemption scope pins, 216 door cells (6 doors x 9 x 4) and 5 edges.\nREST SUITE: pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 gave 3655 tests: 3654 passed, 1 skipped, 0 failed (success true) at 6780cd777.\nTYPECHECKS at bad8baa63: pnpm --filter @objectstack/rest typecheck exit 0 (tsc --noEmit, plus check:test-typecheck: 0 files, 0 errors under tsconfig.test.json). The runtime closure build (@objectstack/runtime^...) exit 0. pnpm --filter @objectstack/runtime typecheck exit 0. Runtime src/domains/meta-item-read-gate-parity.test.ts: 77/77.\nABLATIONS, fix committed first. Each used scripts/ablation-replace.mjs in wrap mode inside a wrapper that traps EXIT, INT and TERM on absolute paths, and each restore was proven (blob == HEAD, git diff HEAD empty).\n(A) Exemption off, in packages/rest/src/meta-item-read-gate.ts: serve(gateInputs.authorExempt ? document : gated.app) became serve(gated.app). Anchor x1 to x0, replacement x0 to x1, blob 817024027bb2 to 6feda3867a6c. 261 run, 3 red, exactly ?layers=true, /layers and /diff x app/crm x author. Restored to blob 817024027bb2 == HEAD.\n(B) Predicate on for everyone, in packages/rest/src/rest-server.ts: mayWriteItem: RestServer.metaSaveVerdict(caller, req.params.type).allowed became mayWriteItem: true. Blob 5942f7eb57de to 43523e878b86. 261 run, 5 red: the three app/crm x non-reader stored-door cells, the book-read-fault edge (its /layers pin) and the org-presentation edge. Restored to blob 5942f7eb57de == HEAD.\n(C) Cross-package reverse verification, in packages/runtime/src/domains/meta.ts: app gate became app pending-decision. Blob c47473b9dcdd to 543aa6707030. Runtime tsc exit 2 with exactly 1 error: TS2322 at meta.ts(297,7), 'pending-decision' not assignable to MetaReadGatePolicy. So the importer reads the rebuilt rest .d.ts. Restored to blob c47473b9dcdd == HEAD.\nPROBE: a one-shot, untracked test file at 9d955a369, run and then deleted, with git status clean afterwards. For an author (manage_metadata, without finance.access), GET /meta/app/crm?state=draft answered 200 with navigation [nav_leads] (the protocol saw state draft), and /layers answered [nav_leads, nav_finance_ledger].\nOBJECTUI CENSUS (Zone 2 item 7, measured before the fix): source at .objectui-sha f8a9d0fb0596, from a read-only blob:none clone in scratch.\n/diff: 0 source readers (MetadataClient.diff has no caller).\n?layers=: 0 source readers (objectui ratchet scripts/__tests__/layered-read-declared-path-4016.test.ts).\n/layers for app: StudioDesignSurface.tsx loads at :1760 and saves navigation at :1960; ResourceEditPage.tsx, the generic editor (no bespoke app EditPage is registered), loads at :1017 and saves at :1500. Both save via client.save, which is PUT /meta/:type/:name, the door whose verdict is the exemption, so no reader saves under a non-author identity.\nDesignerEditorWrapper: CHANGELOG hits only.\nOther /layers readers never load an app: PermissionMatrixEditor (permission), PackageOwdOverviewPanel (object), EmbeddedItemEditor (object sub-items), and StudioDesignSurface :2880 (object) and :3931 (flow).\nPUBLISHED SURFACE (Zone 2 item 4): the @objectstack/rest@17.4.0 tarball's dist/index.d.ts has RestServer 21 times (positive control) and MetaReadGatePolicy|MetaReadGateCaller|createMetaItemReadGate 0 times. Its dist/index.js has createMetaItemReadGate|pending-decision 0 times.\nGATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 92 families from merge base a9fb83ef0. All ran at 9d955a369, each with its output redirected to a log and its exit code read before any pipe. --ran reconciliation: 92 derived, 91 run, 1 NOT-MEASURED (derived from a recorded exit 3), 0 UNRUN.\nNOT MEASURED: pnpm check:dual-build-cjs-loads exits 3, PREREQUISITE NOT MET, because 42 packages have no dist/ and it needs the whole repo built (CI Build Core).\nRED BY DESIGN: node scripts/check-empty-changeset.mjs --base origin/main exits 1, on the DELIBERATE CORRECTION class. The order mandated the in-place correction, and the PR body names the note and what changed.\nRemeasured: check:skill-examples and check:type-check-debt first exited 3 (prerequisite builds). They exit 0 at 9d955a369 after the named builds (client-react closure, runtime closure). check:system-context-census was red after the refactor (a new isSystem read site in metaSaveVerdict) and exits 0 after the row 49 anchor and the --fix count.\nLane additions: pnpm lint (eslint . --no-inline-config, the whole repo) exit 0 in 121s, on the tree that became 9d955a369, so a full run, not a narrowing. The REAL board check node scripts/check-issue-citations.mjs --base origin/main, run after the merge, exit 0 with 17 citations judged and 17 resolving. It first exited 2 on a re-cited unresolvable #10340 in the new docblock, which now cites the landing commit as the file does elsewhere. pnpm --filter @objectstack/spec run check:liveness exit 0.\nPer-gate exits (command → exit code, all at 9d955a369):\nnode scripts/check-adr-0087-registration.mjs --base origin/main → exit 0\nnode scripts/check-adr-0087-registration.mjs --self-test → exit 0\nnode scripts/check-changeset-no-major.mjs --base origin/main → exit 0\nnode scripts/check-changeset-no-major.mjs --self-test → exit 0\nnode scripts/check-ci-filter-parity.mjs → exit 0\nnode scripts/check-closing-keyword-parity.mjs → exit 0\nnode scripts/check-closing-keyword-parity.mjs --self-test → exit 0\nnode scripts/check-comment-mask-adoption.mjs → exit 0\nnode scripts/check-comment-mask-adoption.mjs --self-test → exit 0\nnode scripts/check-comment-mask-corpus.mjs → exit 0\nnode scripts/check-doc-frontmatter.mjs → exit 0\nnode scripts/check-doc-frontmatter.mjs --self-test → exit 0\nnode scripts/check-doc-route-spelling.mjs --advisory → exit 0\nnode scripts/check-doc-route-spelling.mjs --self-test → exit 0\nnode scripts/check-docs-section-name.mjs → exit 0\nnode scripts/check-docs-section-name.mjs --self-test → exit 0\nnode scripts/check-empty-changeset.mjs --base origin/main → exit 1\nnode scripts/check-empty-changeset.mjs --self-test → exit 0\nnode scripts/check-keyed-text-bounds.mjs → exit 0\nnode scripts/check-keyed-text-bounds.mjs --self-test → exit 0\nnode scripts/check-platform-object-tenancy-census.mjs → exit 0\nnode scripts/check-platform-object-tenancy-census.mjs --self-test → exit 0\nnode scripts/check-plugin-teardown-shape.mjs → exit 0\nnode scripts/check-plugin-teardown-shape.mjs --self-test → exit 0\nnode scripts/check-registry-log-declared.mjs → exit 0\nnode scripts/check-registry-log-declared.mjs --self-test → exit 0\nnode scripts/check-rest-log-spy-declared.mjs → exit 0\nnode scripts/check-rest-log-spy-declared.mjs --self-test → exit 0\nnode scripts/check-section-landing-index.mjs → exit 0\nnode scripts/check-section-landing-index.mjs --self-test → exit 0\nnode scripts/check-system-context-census.mjs → exit 0\nnode scripts/check-system-context-census.mjs --self-test → exit 0\nnode scripts/check-undeclared-dep-imports.mjs → exit 0\nnode scripts/check-undeclared-dep-imports.mjs --self-test → exit 0\nnode scripts/docs-audit/check-affected-docs.mjs → exit 0\nnode scripts/docs-audit/check-drift-comment.mjs → exit 0\nnode scripts/pm/release-rehearsal-clone.mjs --self-test → exit 0\npnpm --filter @objectstack/lint run check:doc-formula-expressions → exit 0\npnpm --filter @objectstack/lint run check:doc-security-posture → exit 0\npnpm --filter @objectstack/spec run check:docs → exit 0\npnpm --filter @objectstack/spec run check:duration-unit-keys → exit 0\npnpm --filter @objectstack/spec run check:empty-state → exit 0\npnpm --filter @objectstack/spec run check:liveness → exit 0\npnpm --filter @objectstack/spec run check:skill-examples → exit 0\npnpm --filter @objectstack/spec run check:strictness-ledger → exit 0\npnpm --filter @objectstack/spec run check:variant-docs → exit 0\npnpm --filter @objectstack/spec run check:yaml-examples → exit 0\npnpm check:authz-resolver → exit 0\npnpm check:changeset-gate-self-tests → exit 0\npnpm check:corpus-claim-drift → exit 0\npnpm check:cross-package-test-inputs → exit 0\npnpm check:dispatcher-error-vocabulary → exit 0\npnpm check:doc-anchors → exit 0\npnpm check:doc-authoring → exit 0\npnpm check:docs-audit-scope → exit 0\npnpm check:docs-redirects → exit 0\npnpm check:docs-single-h1 → exit 0\npnpm check:docs-spec-enumerations → exit 0\npnpm check:docs-transcript-drift → exit 0\npnpm check:driver-memory-census → exit 0\npnpm check:dts-closure → exit 0\npnpm check:dual-build-cjs-loads → exit 3\npnpm check:engine-double-contract → exit 0\npnpm check:gitlink-declared → exit 0\npnpm check:issue-citations → exit 0\npnpm check:lean-entry-closure → exit 0\npnpm check:logger-receiver-detach → exit 0\npnpm check:merge-driver → exit 0\npnpm check:nul-bytes → exit 0\npnpm check:objectql-double-limit → exit 0\npnpm check:objectui-changeset → exit 0\npnpm check:org-identifier → exit 0\npnpm check:page-declaration-shape → exit 0\npnpm check:pm-changeset-deadline-census → exit 0\npnpm check:pm-widening-tells → exit 0\npnpm check:published-files → exit 0\npnpm check:published-readme-links → exit 0\npnpm check:query-options-erasure → exit 0\npnpm check:react-page-adapter-contract → exit 0\npnpm check:refd-timer-probe → exit 0\npnpm check:role-word → exit 0\npnpm check:route-envelope → exit 0\npnpm check:skill-identifier-liveness → exit 0\npnpm check:slot-lookup → exit 0\npnpm check:sourcemap-no-sources-content → exit 0\npnpm check:test-source-alias → exit 0\npnpm check:tier-file-adoption → exit 0\npnpm check:type-check-coverage → exit 0\npnpm check:type-check-debt → exit 0\npnpm check:vendor-version-stamps → exit 0\npnpm check:watch-hint-literal → exit 0\npnpm check:where-matcher → exit 0", "mcp_calls": "0 — no MCP GitHub tool was called (all reads went through public REST via curl, and all writes through the scripts/pm relay tools).", "api_writes": "3 relay writes, each ONE repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by .github/workflows/fleet-write.yml as objectstack-fleet[bot]:\n(1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft forced): request fw-20260927T183620Z-99da70, run 36341271096, success.\n(2) label-write --assign os-litant, which is POST /repos/objectstack-ai/objectstack/issues/20284/assignees: request fw-20260927T183705Z-424aea, run 36341313894, success, read back os-litant. No label written; the size, documentation, tests and tooling labels came from the labelers.\n(3) This os-dev-report comment, via post-stamped, which is POST /repos/objectstack-ai/objectstack/issues/20156/comments.\nNot REST: git push of the branch (the empty-branch probe first, then 6 pushes, the last at head 9d955a369).", "open_questions": [ { "question": "Confirm the in-place correction of the pending release note .changeset/20156-alternate-door-read-gates.md. Its bullet \"One case is unchanged in this release … pending a decision\" and its closing \"except the app row …\" sentence now state the ruled answer. check-empty-changeset, and therefore Check Changeset, stays red by design on that file.", "options": [ "A: confirm the correction. The gate stays red, and Check Changeset is outside the seven required contexts.", "B: restore the note from the merge base and let the new changeset alone state the ruled answer." ], "recommendation": "A, because the order mandated the in-place correction (Zone 2 item 6), and the gate's own remedy text says restoring would republish a sentence this PR makes false. The two pending notes would then contradict each other in one release." } ], "out_of_scope_findings": [ "class: a · reach: named producer + public door measured. The producers are objectui (f8a9d0fb) app-shell StudioDesignSurface.tsx, which merges { ...layered.effective, ...getDraft } at :1760-1767 and saves with client.save('app', …, { mode: 'draft' }) at :1960, and ResourceEditPage.tsx, which merges at :1016-1056 and saves at :1500. The door is GET /meta/app/crm?state=draft (the plain read). At 9d955a369 it answered an author holding manage_metadata but not finance.access navigation [nav_leads], while /layers answered [nav_leads, nav_finance_ledger]. Evidence: when an app has a pending draft, the draft's PRUNED navigation overrides the author's whole effective navigation, and the next draft save drops the entries withheld from that author (by requiredPermissions or the docs audience). The ruling keeps the plain read pruned for authors, so the seam is the editors reading their edit baseline through a read-to-display door. Where to fix it (objectui reading the draft through a stored-version door, or the server serving the draft layer there) is the seat's call; the fix likely lands in objectui. Dedupe words: app draft state=draft pruned for author · designer getDraft merge effective navigation save drops entries · StudioDesignSurface doNavSave withheld nav entries · ResourceEditPage draft baseline pruned app", "carrier: 承接者:无 · noted, not filed (PR Acceptance notes): content/docs/plugins/adding-a-metadata-type.mdx:164-196 documents DesignerEditorWrapper, which is absent from objectui source at f8a9d0fb (CHANGELOG hits only), and a ?layers=true read that objectui's own ratchet forbids. Docs drift.", "carrier: 承接者:无 · noted, not filed (PR Acceptance notes): the layered view serves the protocol's _diagnostics beside the pruned layers. The plain read's pruned document carries the same _diagnostics, computed over the unpruned document, so this adds no new disclosure. Whether any message names a withheld entry is NOT MEASURED." ] }objectstack-fleet commentedon Sep 27, 2026
ACCEPT: PR #20284 at
9d955a36, R3 of thedomain:cliseat (executing ruling5856774816, letter B)domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review of record, written 2026-09-27T19:00ZReviewed against GitHub and
origin/main, not against the report.Checklist:
main. The first line isFixes #20156(ruling item 5), then a bare line-initialClause-②: no. [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 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 are named only as remaining open.check-governed-merges --pr 20284: NOT governed; 640 changed lines):meta-item-read-gate.ts;rest-server.ts;content/docs/ui/apps.mdxandcontent/docs/permissions/system-context.mdx.The claim
5857413915was amended in place to name the two docs pages. Each was owed by this change: a sentence made false, and a census row the movedisSystemread turned red. No runtime,metadata-coreorspecfile is touched.RestServer.metaSaveVerdict: the save door's full caller admission, spelled once, and called byPUT /meta/:type/:nameitself. It reaches the gate as the caller propertyMetaReadGateCaller.mayWriteItem, in ADR-0106 D4's shape./layers,?layers=trueand/diffget the plain read's pruned answer, not a 403.PENDING_DECISIONand its docblock are retired. The ruled cells and an author control replace them, with scope pins for exactly three doors, onlyapp, and the whole refusals kept.patch+Clause-②: no./diffdiscarded a serve verdict's document, so the policy flip alone would have pruned nothing. Both now emit the judged documents, and for every other type this is a no-op.'pending-decision', which proves it reads the rebuilt type..objectui-sha. Two/layersapp readers exist, and both save throughPUT, which is the exemption's own predicate. There is no/diffor?layers=reader.9d955a36: 32 checkssuccess, includingTypeScript Type Check, allTest Coreshards andLint & Repo Gates. 2 are skipped:Console Pin GateandPacked-tarball smoke (opt-in), both rostered expected skips. 1 is red:Check Changeset, the DELIBERATE CORRECTION class, recorded on the PR as5858794214(see below).5858783654). ItsServed-tier:line was drafted as a tier description; the seat set it to the constantCONTRACT_REVIEW_TIER, because the agent was launched at, and served by, that tier (read from the agent's own transcript). Nothing else in the record was changed.Decisions of record:
.changeset/20156-alternate-door-read-gates.mdis confirmed by the same-head review record, which judges all 6 rewritten sentences true of the combined release (landing-operations.md: 「同 head 达档复核 PASS 记录即确认,⛔ 不等维护者」). This answers the dev's open question as A.pr-automation.ymlhas nomerge_grouptrigger, andCheck Changesetis not a required context;5858794214).The gate itself is unchanged.
patch+Clause-②: nostands, as ruled.MetaReadGatePolicyandMetaReadGateCallerare absent from the published@objectstack/rest@17.4.0(registry tarball, 0 hits), and they first publish through the pending.changeset/20193-dispatcher-meta-read-gate.md.Out of scope:
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. An app author's draft baseline is read through the pruned plain read (?state=draft), and the designers merge it over the whole stored app, so a draft save drops the withheld entries. The review confirmed the server half at source. It is filed bare for triage, which decides the carrier (server or objectui). It is not caused by this PR:mainalready prunes that read for every caller.Acceptance notes, not filed:content/docs/plugins/adding-a-metadata-type.mdxdocuments aDesignerEditorWrapperthat objectui no longer has, and a?layers=trueread that objectui's own ratchet forbids (docs drift);_diagnosticsbeside the pruned layers is the same_diagnosticsthe plain read already serves.Lane order: once this lands, #20237 (p1, the dispatcher's LIST branch) takes the
rest-server.ts/ gate-module fence, and #20139 (p2) follows it.Next: landing through the relay ops
pr_ready+automerge_enable.Generated by Claude Code
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…