Skip to content

[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

Closed

Description

@objectstack-fleet

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):

  • serveMetaItemLayered is reached from both layered entry points (the /layers route 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

Reach and context

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:

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

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20190 at 8116184f, R3 of the domain:cli seat (after patch rounds 1 and 2)

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review of record, written 2026-09-27T10:09Z

    Reviewed against GitHub and origin/main, not against the report.

    Checklist:

    Decisions of record:

    • Seat sequencing (5853865127, upheld in 5854384500): 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:

    After landing: this card stays open (Part of). It moves to needs-user-decision for the partial app cells, with options A / B / C and the reviews' measurements.

    Next: landing through the relay ops pr_ready + automerge_enable.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20190 → 585c9af587a165c066c7815185a6652c2b9c545e (Part of: this card stays open)

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · landing record, written 2026-09-27T10:30Z


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    需要维护者裁决:设计器读 app 时,对「只能看到 app 一部分」的人该怎么答

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · 决策卡落卡,written 2026-09-27T10:31Z。⛔ 席位不裁这个问题:它划的是一条新的权限边界。

    状态转换(本评论同笔): pm:dispatched → needs-user-decision,assignee 清空。PR #20190 已按 Part of #20156 落地(合并 585c9af5),本卡其余部分都已交付,只剩下面这一格等裁决。

    一句话问题

    一个 app 里,有些导航条目是某些成员无权看的。成员打开 app 时,那些条目被隐藏了。可是 Studio 设计器读 app 用的是另外三个入口,那里目前把完整 app 原样返回给任何能打开这个 app 的人,于是被隐藏条目的名字在那里仍然看得见。要不要在这三个入口也隐藏?隐藏的话,设计器保存时会把看不见的条目删掉。

    背景与已交付部分

    Governing text:

    • ADR-0106 D4(对象 schema 遮罩):isSystem 与平台管理员豁免,「Studio/Setup authoring requires the full schema… The exemption is a caller property, not a route property」。这是对象的先例,没有覆盖 app。
    • ADR-0045 §3(未发布 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。

    • 逐条读过,没有一条裁到「app 的设计器入口对部分可见调用者」这一格。
    • ADR-0106 D4 最接近,但它只管对象 schema 遮罩。
    • ADR-0068 D4 管平台运营动作与 AI 授权的角色门,与本问题无关。

    带复核命令的前提:

    • 3 格仍是待裁状态: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 项,不含任何业务权限)。
    • app 过滤没有管理员豁免:git grep -n "reqApp.every" origin/main -- packages/rest/src/rest-server.ts。

    选项 × 真实代价

    选项 做什么 客户能感知到的后果
    A 拒绝 三个入口对部分可见调用者答 403 平台管理员只要缺某个条目的权限,就打不开这个 app 的设计器(契约复核已实测:管理员能力清单无通配,app 过滤无管理员豁免)
    B 作者豁免 有权写这个 app 的人(metaWriteCapabilityVerdict 放行者)看到完整存储版本;其他人拿到普通读的裁剪版本 作者照常设计、保存不丢条目;普通成员在这些入口上只看到自己能看的条目
    C 裁剪 + 写侧守卫 所有人拿裁剪版本;保存时,如果会删掉作者看不见的条目,就拒绝这次保存 作者在设计器里看不到部分条目,保存可能被一个看不懂的理由拒绝
    D 维持现状 保持现在的待裁状态 成员仍能在这三个入口看到被隐藏条目的名字(p2 暴露)

    业务含义直译

    • A: 像是把「看完整菜单」和「有每道菜的试吃权」绑在一起,连店长都可能被挡在厨房外。
    • B: 像 Salesforce 的 Lightning App Builder。能改应用的管理员看到全部导航项;普通用户只看到自己有权的项。
    • C: 编辑者看的是残缺的稿子,还不能存,等于把问题推给了编辑者。
    • D: 什么都不做,继续让普通成员看到不该看到的条目名。

    四轴分析(业务立场)

    • ① 项目长远合理性(推荐只按此轴得出): 两年后,平台对「读来展示」和「读来编辑」应当有两条清楚的规则:展示按每个用户裁剪,编辑按编辑能力整体给。主流平台都这样建模,例如 Salesforce(App Builder 与用户可见性分离)、ServiceNow(应用设计者看完整应用定义)。ADR-0106 D4 已经为对象 schema 定了同一条规则:「谁要编辑,谁就得看全」。B 把这条规则延伸到 app,平台上就只有一条规则,而不是对象一条、app 另一条。A 与这条规则相反,C 用写侧补丁弥补读侧的缺口。只看①选 B。
    • ② 实际业务拉动: 真实使用这些入口的只有 Studio 设计器(DesignerEditorWrapper 走 ?layers=true,已在文档中实测到)。普通成员的界面不调用这三个入口,所以隐藏条目只影响直接调 API 的成员。今天的暴露只是条目名,没有条目内容,分诊定为 p2。
    • ③ 防 AI 犯错: AI 作者(持写能力)在 B 下拿到完整 app,不会因为看不见而在保存时静默删掉条目。在 A 下,AI 作者会被响亮拒绝,但这是错拒,会逼它去绕路。在 C 下,AI 会被一个它无法理解的写侧拒绝卡住。D 让出错被静默掩盖:成员看到了不该看到的名字,没有任何报错。
    • ④ 创业阶段不扩散: B 复用已有的 metaWriteCapabilityVerdict,只改一个分支,不新增门禁,也不新增错误码。C 要新造写侧比对机制和一个新的拒绝。A 代码最少,但会产生管理员被挡的支持成本。D 零成本,但留着暴露。

    os-decision-facets

    • ① 长远合理性:B 缩小特例:对象与 app 共用「编辑者看全、展示按人裁剪」一条规则;A、C 各自新增一条 app 专属规则。
    • ② 业务拉动:唯一真实消费者是 Studio 设计器(?layers=true 读入后存回),必须拿到完整 app;普通成员界面不走这三个入口。
    • ③ 防 AI 犯错:B 下 AI 作者看全、不会静默删条目;D 是静默暴露;A 是错拒;C 是 AI 难以理解的写侧拒绝。
    • ④ 创业阶段不扩散:B 复用现成的写能力判定,只改一个分支,零新门禁;C 新增写侧机制与拒绝。
      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;②③④ 是否翻转:否。

    置信缺口:

    • 尚未实测 objectui(Studio 之外)有没有其他非作者界面读这三个入口。
    • 豁免判定用 metaWriteCapabilityVerdict(谁能写这个 app),还是 ADR-0106 D4 字面的「平台管理员」判定,是一个执行参数,请一并裁定。席位推荐前者,理由是能存回的人必须看全,否则保存会丢条目。

    裁后执行

    • 裁 B: 本席开一个新派发(本卡的后续 PR)。
      • 在 metaItemReadGate 的 pending-decision 分支按裁定的判定放行作者,其余调用者给普通读的裁剪版本。
      • 普查里的 3 格改成裁定后的答复,并加一格作者控制;changeset 取 patch。
      • 本卡随该 PR 以 Fixes 关闭。
    • 裁 A、C: 同样由本席派发,按对应形状实施;C 另需写侧拒绝,要复用已登记的错误码,不新增。
    • 裁 D: 本卡关闭为 not planned,把 3 格的待裁标记改成「已裁维持」并引用这条裁决。

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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 (the domain:cli seat'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/main ab820016b3: the census packages/rest/src/meta-alternate-door-read-gates.test.ts:302-329 declares the three partial-app cells as PENDING_DECISION with anchor #20156; metaWriteCapabilityVerdict is imported at rest-server.ts:69 and 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 over AGENTS.md, docs/adr, packages/spec/src for the designer-door / caller-property terms: ADR-0106 D4 is the only hit, and it does not cover app.

    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:

    1. Exemption predicate: metaWriteCapabilityVerdict for 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.
    2. Non-authors on the three doors get the plain read's pruned answer, not a 403: the doors answer what the plain read answers, which is the census's own rule.
    3. Census: the three PENDING_DECISION cells 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.
    4. Changeset patch for @objectstack/rest; Clause-②: no (no published accept set moves; the doors' answers narrow to the plain read's for non-authors, a permission fix).
    5. The domain:cli seat dispatches the follow-up PR as its analysis says; the card closes by Fixes with it.

    Confidence gap carried: whether any objectui surface other than Studio's DesignerEditorWrapper reads these doors is unmeasured; the dev measures it first and reports a second reader rather than tolerating it.

    State: needs-user-decision → pm:queue in this act (domain:cli · p1 · security unchanged); body first line set to this ruling.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ⏸️ 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 keeps needs-user-decision, its body carries no Ruled: 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

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ 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 · security unchanged); the body's first line now carries Ruled: 5856774816 · letter B. Execution follows the 裁后执行 section of the ruling.


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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_01UYBdGBzWSrAMzpW8ah3GbP
    Account: os-litant
    Branch: claude/issue-20156-app-author-exemption
    Worktree: objectstack-issue-20156
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/rest/src/meta-item-read-gate.ts (MetaReadGatePolicy.app's pending-decision arm becomes the ruled author exemption, plus whatever caller or source field carries the write verdict), packages/rest/src/rest-server.ts (STORED_VERSION_DOOR_POLICY and 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_DECISION retires; ruled cells plus an author control), packages/rest/src/execctx-consumer-census.test.ts only if a site count moves, packages/runtime/src/domains/meta.ts and its parity test only if the shared gate's signature forces the runtime caller to adapt, packages/spec/liveness/*.json anchor 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) and content/docs/permissions/system-context.mdx (row 49 re-anchored to metaSaveVerdict, because check:system-context-census counts the moved isSystem read site). packages/metadata-core/** (metaWriteCapabilityVerdict, domain:engine) is read-only, and so is the rest of packages/spec/**. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default tier. dispatch-gates.mjs --tier names 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 disjoint

    Taken 2026-09-27T15:53Z, origin/main after 2bcd5cfe. Executes ruling 5856774816 (batch #227 item 1, letter B, confirmed 5856866273, maintainer 「同意」). On /layers, ?layers=true and /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-②: no and patch, per ruling item 4. The card closes by Fixes (item 5). The earlier branch claude/issue-20156-alternate-door-read-gates landed 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

  9. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
      ]
    }
  10. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20284 at 9d955a36, R3 of the domain:cli seat (executing ruling 5856774816, letter B)

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review of record, written 2026-09-27T19:00Z

    Reviewed against GitHub and origin/main, not against the report.

    Checklist:

    • PR shape: draft, base main. The first line is Fixes #20156 (ruling item 5), then a bare line-initial Clause-②: 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.
    • Scope: 7 files, none governed (check-governed-merges --pr 20284: NOT governed; 640 changed lines):
      • the gate, meta-item-read-gate.ts;
      • the transport, rest-server.ts;
      • the census test;
      • the corrected pending changeset and one new changeset;
      • content/docs/ui/apps.mdx and content/docs/permissions/system-context.mdx.
        The claim 5857413915 was amended in place to name the two docs pages. Each was owed by this change: a sentence made false, and a census row the moved isSystem read turned red. No runtime, metadata-core or spec file is touched.
    • Fix, per the ruling's execution parameters:
      • (1) The predicate is RestServer.metaSaveVerdict: the save door's full caller admission, spelled once, and called by PUT /meta/:type/:name itself. It reaches the gate as the caller property MetaReadGateCaller.mayWriteItem, in ADR-0106 D4's shape.
      • (2) Non-authors on /layers, ?layers=true and /diff get the plain read's pruned answer, not a 403.
      • (3) PENDING_DECISION and its docblock are retired. The ruled cells and an author control replace them, with scope pins for exactly three doors, only app, and the whole refusals kept.
      • (4) patch + Clause-②: no.
      • The dev also found and fixed a mechanism the order did not anticipate. The layered view and /diff discarded 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.
    • Tests:
      • the census, 261/261;
      • ablation A (exemption off) turns exactly the 3 author cells red;
      • ablation B (the exemption for everyone) turns 5 cells red;
      • ablation C: the runtime importer fails to compile on the retired 'pending-decision', which proves it reads the rebuilt type.
    • The ruling's carried confidence gap was measured before the fix, in objectui source at .objectui-sha. Two /layers app readers exist, and both save through PUT, which is the exemption's own predicate. There is no /diff or ?layers= reader.
    • CI on head 9d955a36: 32 checks success, including TypeScript Type Check, all Test Core shards and Lint & Repo Gates. 2 are skipped: Console Pin Gate and Packed-tarball smoke (opt-in), both rostered expected skips. 1 is red: Check Changeset, the DELIBERATE CORRECTION class, recorded on the PR as 5858794214 (see below).
    • Contract review: at-tier independent, PASS (5858783654). Its Served-tier: line was drafted as a tier description; the seat set it to the constant CONTRACT_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:

    • The DELIBERATE CORRECTION of .changeset/20156-alternate-door-read-gates.md is 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.
    • The PR lands under the three conditions for a red-by-design check:
      • the gate's own source says it stays red for this class;
      • pr-automation.yml has no merge_group trigger, and Check Changeset is not a required context;
      • the gate and its reason are written on the PR (5858794214).
        The gate itself is unchanged.
    • patch + Clause-②: no stands, as ruled. MetaReadGatePolicy and MetaReadGateCaller are 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:

    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

  11. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20284 → 2dccb7d494ffc9190b7df36b4d91e94e9610a28a

    domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · landing record, written 2026-09-27T19:21Z


    Generated by Claude Code

  12. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling B here, item 2, narrowed for two doors by ruling B on #20378 (batch #234 item 2, 5865708652) · 2026-09-28T07:52Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Record only; nothing else on this card moves.

    • /diff and /history become authoring doors: a caller mayReadPendingDrafts does not admit is refused 403 as /meta/_drafts refuses, 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_history cannot 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.
    • /layers and ?layers=true are unchanged: they read the active row only and keep item 2's pruned plain-read answer as ruled here.
  13. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling B (5856774816) narrowed for /diff and /history: per ruling 5865708652 on #20378 (letter B, the maintainer's 「同意」 in the director seat's batch #234, 2026-09-28), GET /meta/:type/:name/diff and GET /meta/:type/:name/history are authoring doors. A caller mayReadPendingDrafts does not admit is refused as GET /meta/_drafts refuses, because the history store cannot tell a draft save from an active save. /layers and ?layers=true keep this ruling's pruned plain-read answer. Posted by the domain:cli seat (#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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions