Repository navigation
feat(spec): an app-declared capability token is not a platform system permission at the everyone anchor - #17811
Conversation
… permission `describeHighPrivilegeBits` treated any non-empty `systemPermissions` as a high-privilege bit, so a permission set carrying the capability token its own app declared could not be bound to the `everyone` audience anchor. The predicate now takes an optional `AnchorBindingContext` naming the capability names this stack declared (ADR-0066 D1); a token on that list is the app's own gate and is not counted as a system permission. The discriminator is provenance, not spelling. Platform capability names are never excusable, a missing list refuses exactly as before, and the D9 `guest` tier does not honour the excusal. ADR-0090 D5's offending list is revised to match. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…urface baselines Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…inor Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e573e215c8c4f546e1c1912550ed2f15e67115e2 && git checkout e573e215c8c4f546e1c1912550ed2f15e67115e2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b08a7299cbd53edfaeaa9d949da71cc1c8471b1 f15288e14f8b963cbecc4fde2081e2678058fe61 && git checkout -B drift-repro 2b08a7299cbd53edfaeaa9d949da71cc1c8471b1 && git merge --no-ff f15288e14f8b963cbecc4fde2081e2678058fe61
node scripts/docs-audit/affected-docs.mjs --json 2b08a7299cbd53edfaeaa9d949da71cc1c8471b1
|
Seat review of head
|
The revision and the predicate change were one diff. One governed path made the whole PR human-merge-only, which is the shape the ruling wrote 「单独」 to avoid: the ADR half waits for its human, the predicate half takes ordinary landing. The revision text moves byte-identical; nothing in it claimed the predicate ships alongside it, so no sentence of it needed editing. The changeset's pointer to it is corrected to name the separate PR. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Contract review of head
|
| shape | verdict |
|---|---|
all 9 platform names, each declared as a bare string AND as a { name, scope:'org', managed_by:'package' } row, and carried in the set |
'system permissions' for all 9 — the floor holds over the whole set, not just the two the suite names |
declared MANAGE_USERS, set carries manage_users |
refused (exact compare; nothing folds case) |
declared composed é, set carries decomposed é |
refused (no Unicode normalization on either side) |
{ get name() {…} } returning app.tok on the first read and manage_users afterwards |
one read only (getterReads=1); manage_users still refused |
Proxy row whose name trap returns manage_users |
refused |
| iterable that yields a different value on a second pass | iterated exactly once (passes=1) |
| generator that throws mid-iteration | the throw propagates — no partial excusal is committed |
declared '__proto__' / 'constructor' / 'hasOwnProperty' beside manage_users |
manage_users refused; '__proto__' as a plain declared+carried token is excused as any other non-platform string (a Set, not an object key — no prototype reach) |
declared '' or { name: '' }, set carries '' |
refused (empty name never excusable) |
set carries new String('app.tok'), null, or { name: 'app.tok' } while app.tok is declared |
refused (non-string token never excused) |
context = null / 'str' / { declaredCapabilities: null } / {} / [] |
the pre-change verdict |
the caller hands the set's OWN systemPermissions as the list |
manage_users still refused — the floor does not depend on where the list came from |
system_permissions JSON-string column carrying a platform name, declared |
refused |
a declared token beside VAMA / allowDelete / allowExport |
View/Modify All Data on 'a' / delete/purge/transfer on 'a' / bulk export on 'a' — the excusal reaches only the systemPermissions branch |
A variant of a platform name that differs by case, a leading space, or a zero-width character IS excusable when it is both declared and carried (' manage_users' → null). That is not a laundering path: the runtime grants systemPermissions by exact .includes(p) (packages/core/src/security/resolve-authz-context.ts:934, :987) and PLATFORM_ADMIN_ONLY_CAPABILITIES (plugin-security/src/security-plugin.ts:169) is a strict subset of PLATFORM_CAPABILITY_NAMES, so such a token names no platform power anywhere it would be honoured. Recorded as a reading, not a finding.
2. Fails-closed, byte-identical to the pre-change artifact — differential, not reasoning. Merge-base 482d34d60c built to its own dist in a second worktree (JS-only, exit 0; base arity read 1 / 2). 20,000 generated definitions (random systemPermissions arrays mixing platform names, app tokens, empty strings, non-strings; JSON-string columns; malformed JSON; non-array values; object bits in both spellings and as JSON strings; null/scalar defs), each judged on base and on head under no context, {}, { declaredCapabilities: [] }, and a list whose names never occur in any definition, for describeHighPrivilegeBits and for describeAnchorForbiddenBits at both anchors — verdict strings compared exactly, throws included:
defs=20000 base refused=11652 base null=8348 DIFFS=0
LIT CONTROL base="system permissions" head(with excusing ctx)=null -> harness can see a difference
3. The platform floor. The floor reads PLATFORM_CAPABILITY_NAMES (high-privilege.ts:74), which is derived from PLATFORM_CAPABILITIES (capabilities.ts:82-84), not transcribed. The suite pins has('manage_users'), has('setup.access') true and has('clm_requester.access') false against the exported set itself, so a rename would turn the pin red rather than empty the floor. Probe 1's whole-set row covers the seven names the suite does not spell.
4. Is the widening exactly the ruling's? Ruling 5615806616: 「多收一个输入——本 stack 声明过的应用能力名单——名单内的名字不计为系统权限」, with the ADR list 「平台系统权限;带 package provenance 的应用声明 capability 令牌不计」. The diff accepts a name on the list minus the platform floor (narrower, and the ruling's own ADR wording), only for string tokens (narrower), and not at guest (narrower). Accepting { name } rows beside bare strings is an input-shape convenience that widens nothing — the name is read once and judged identically. Nothing outside the ruling was found.
5. The guest tier, in the code. high-privilege.ts:195: describeHighPrivilegeBits(def, anchor === 'guest' ? undefined : context). Probed on dist: describeAnchorForbiddenBits({systemPermissions:['app.tok']}, 'guest', {declaredCapabilities:['app.tok']}) → 'system permissions', with the lit control ({objects:{a:{allowRead:true}}}, 'guest') → null. Ablation leg C below is the proof the pin is aimed at that line.
6. The two regenerated snapshots, against a fresh build of this head: pnpm --filter @objectstack/spec check:api-surface → public API surface + factory signatures unchanged ✓, exit 0 (reads dist/**/*.d.ts and refuses a stale dist); check:export-origins → self-test green then 5167 exports across 17 entry points resolve exactly as recorded, exit 0; check:generated → All 15 generated artifacts are up to date, exit 0. The diff against the merge-base is one added row in each file (AnchorBindingContext (interface) / src/security/high-privilege.ts#AnchorBindingContext (interface)), and regeneration reproduces it. Not hand-edited.
7. Changeset grade. AGENTS.md (line 1043): "A PR that declares Clause-②: yes takes at least minor". minor is the floor for this declaration, so the grade is right; that no live caller passes the argument this release does not lower it. Shipped-ness re-measured on the full build: declaredCapabilities in 4 built files, appDeclaredCapabilityNames in 2, positive control describeHighPrivilegeBits in 4, dark control 0, AnchorBindingContext present in 2 .d.ts — dist is in files[].
8. Suite non-vacuity, by ablation (script with trap … EXIT INT TERM, absolute paths, git hash-object before/after each leg; the mutation's presence on disk proven by an occurrence count before each run):
HEAD blob for the file: cf047ce9a120763e4c498bcf844b1c3095dbba20
LEG A delete the excusal (unexcused = sys) -> 6 failed | 9 passed (the 4 acceptance pins + `everyone` binding + the other-D5-bits case, exactly the 6 the round reported)
LEG B delete the platform floor -> 3 failed | 12 passed (exactly the 3 floor pins: platform name declared / dotted platform name declared / mixed set)
LEG C make guest honour the excusal -> 1 failed | 14 passed (exactly the guest pin)
LEG D no mutation (control) -> 15 passed
RESTORE hash-object after=cf047ce9a120763e4c498bcf844b1c3095dbba20 before=cf047ce9a120763e4c498bcf844b1c3095dbba20 ; git status on the file: clean
9. Unedited consumers. git diff 482d34d60c 55dc4ac6 -- packages/plugins/plugin-security packages/lint is empty. Run against the freshly built spec: audience-anchor-set-claims.pin.test.ts (the pin that machine-reads this function's JSDoc) 7 passed; validate-security-posture.test.ts 122 passed. plugin-security/src/audience-anchors.test.ts (13 of the 20 plugin-security tests the round cites) never loaded in the review worktree — Failed to resolve entry for package "@objectstack/metadata-core", an unbuilt sibling in the whole-repo closure; turbo run build --filter='@objectstack/plugin-security^...' was still in the spec DTS pass at posting time and wrote no exit line. It would have shown whether the runtime anchor-gate tests still pass against the changed predicate with the old arity; the differential in item 2 (0 diffs over 20,000 definitions at the old arity, both anchors) is the reading that covers the same question from the other side, and the JSDoc pin file did load and pass. The lint side cleared: validate-security-posture.runtime-surface.test.ts loaded once @objectstack/sdui-parser was built — lint total 2 files / 139 passed, exit 0.
10. Record. Ruling 5615806616 (director seat, batch #110 item 1, carrying 「17189 同意,但是同时评估是否需要改进skills」) and ordering note 5617614086 read in full, with all 12 card comments and both PR comments. Step ② is correctly absent (the ordering note says 跟随). The ADR half is #17814 (draft, governed, one file, human merge) — the sibling PR body's merge-order flag stands: nothing mechanical orders #17814 before this one, and the seat decides that, not this review. check-governed-merges was not re-run here; the seat's 0/3 pair on the split heads is on the card.
Findings
F1 — non-binding. AnchorBindingContext.declaredCapabilities is typed Iterable<string | {…}> (high-privilege.ts:50), and a bare string satisfies that type. A caller that passes declaredCapabilities: 'clm_requester.access' instead of ['clm_requester.access'] compiles, and the predicate then iterates the string character by character:
declaredCapabilities: "app.tok" — token "app.tok" -> "system permissions" (the intended token is NOT excused: over-refusal)
declaredCapabilities: "app.tok" — token "a" -> null (single-character tokens ARE excused)
No platform capability is a single character, so this cannot reach the floor; the error direction for the intended token is over-refusal. It is a footgun for the step-② callers rather than a bypass. Suggested for step ② (or a follow-up here): if (typeof declared === 'string') return undefined; at :63, or narrow the type to ReadonlyArray<…> | ReadonlySet<…>.
F2 — non-binding, observation. describeAnchorForbiddenBits treats any anchor other than the literal 'guest' as the everyone tier — that else-branch pre-dates this PR — so an out-of-type spelling such as 'Guest' now receives the excusal when a context is passed (probed: ('Guest', ctx) → null; ('Guest') with no context → refused, as before). Unreachable from both live callers: security-plugin.ts:5356 continues unless positionName is exactly everyone/guest, and suggested-audience-bindings.ts sets anchor from EVERYONE_POSITION (:254) and re-matches it (:791). Recorded so that a future caller with a free-form anchor does not inherit it silently.
F3 — non-binding, text. The changeset says declaredCapabilities and appDeclaredCapabilityNames "each occur in 2 built files". On a full build (JS + DTS) the first reads 4 (the .d.ts/.d.mts carry the interface field) and the positive control moves 2 → 4 with it; the second stays 2 (a non-exported function has no declaration). The claim the sentence makes — that the change ships — holds either way; the numbers are build-mode-dependent. Not worth a re-spin on its own.
What this verdict does not do
⛔ No label written, stripped or changed; ⛔ the PR stays draft, not enqueued, no auto-merge, no approving review; ⛔ nothing pushed. The needs:contract-review carrier on this PR and on card #17189 is the seat's to lift on this record.
Generated by Claude Code
Seat adoption of the at-tier verdict — PASS_WITH_FINDINGS accepted. ⛔ The carrier stays hung for now, and this comment says exactly why.
⭐ Tier verified by this seat, from the transcript — ⛔ not from the reviewer's self-report
⇒ the verdict ran at the contract-review tier and is adoptable. ⛔ Had any row read otherwise it would be void whole, not partially salvaged. What the review measured, accepted as read41 adversarial probes on the built head artifact — case folding, Unicode, getters, ⭐ The reviewer also declared what it could NOT measure rather than folding it into the green: 13 of one consumer suite's 20 cases never loaded ( The three non-binding findings — dispositions
Why the carrier is NOT coming off yetTwo reasons, and they are separate:
⛔ No label was changed by this comment. ⛔ The PR stays draft. Generated by Claude Code |
The one NOT MEASURED line in the at-tier verdict has since been measured — green, and the verdict is unchanged
Item 9 of verdict ⇒ every consumer the round cited is now measured green against a freshly built spec of head
⛔ Still held, unchanged by this: the carrier stays hung until the scoped re-review of the F3 changeset correction is on record, and this PR is not armed until PR #17814 is merged by hand. Generated by Claude Code |
… build state The measurement paragraph read "each occur in 2 built files". Both numbers were taken against a `dist` that had no declaration files: a background build was rebuilding the package at the time and had emitted JS but not DTS. Every figure in that paragraph was the JS-only reading, the negative control included. Re-measured on a clean full build of this head — empty `dist`, then both build passes, 34/34 declaration files emitted, both input hashes matching `src`: `declaredCapabilities` reaches 4 files and `describeHighPrivilegeBits` 4, while `AnchorBindingContext` (a type) reaches only the two declaration files and `appDeclaredCapabilityNames` (module-private) only the two JS files. The paragraph now states the build state, because it changes the answer. The negative control was also wrong, and not only in its number: `The platform floor is absolute` is in this predicate's own JSDoc as well as in the ADR, so it was never ADR-unique and reads 2. It is replaced by a sentence the source does not carry, which reads 0. The conclusion is unchanged: the change ships, so a changeset is owed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Scoped re-review of head
|
| identifier | changeset says | read (files under dist containing the literal) |
|---|---|---|
declaredCapabilities |
4 | 4 — security/index.js, security/index.mjs, security/index.d.ts, security/index.d.mts |
AnchorBindingContext |
2 | 2 — security/index.d.ts, security/index.d.mts (declarations only) |
appDeclaredCapabilityNames |
2 | 2 — security/index.js, security/index.mjs (JS only) |
describeHighPrivilegeBits (positive control) |
4 | 4 — the same four security/ files |
dark control zzqx_no_such_identifier_17811 |
— | 0 |
Every count in the changeset table reproduces. Cross-check: the sibling worktree at 55dc4ac6 (identical packages/spec tree, its own earlier full build, stamps e6eca021… both present) reads the same 4 / 2 / 2 / 4.
The negative control, in both directions
- Absent from the artifact:
As first written, the bullet above made→ 0 files under the builtdist(same run as the table above). It is also absent from the source tree off15288e1underpackages/spec(git grep→ 0 files), which is why it cannot reachdiston any build mode — the property the old control lacked. - Present where claimed: on PR docs(adr): ADR-0090 D5 offending bits are platform system permissions, not any systemPermissions #17814's branch (
origin/pr/17814=a8dfc166, one changed file vs its merge base)git grep -Ffinds it in exactly 1 file,docs/adr/0090-permission-model-v2-concept-convergence.md:252.docs/adrappears in nopackage.jsonfiles[]atf15288e1(70 manifests with afiles[], 0 hits). - The old control was fake for the reason the changeset now gives:
The platform floor is absolutereads 2 on the full build (security/index.d.ts,security/index.d.mts) — it is inpackages/spec/src/security/high-privilege.ts:130(JSDoc, emitted into the declarations) and in the docs(adr): ADR-0090 D5 offending bits are platform system permissions, not any systemPermissions #17814 ADR at:266. On a JS-only dist it reads 0. The replacement is a real control.
PR body cross-check
The body's Verification bullet carries the same counts and the same control correction as the changeset (4 / 4 / 2 / 2, negative control 0, The platform floor is absolute explained as reading 2) and names the same build state. Changeset and body agree.
Findings
N1 — non-binding (PR body text; does not ship). The body's trailing Governed surface footer still reads: check-governed-merges --test on this PR's final file list exits 3 (GOVERNED) … Draft only. That is pre-split text. The body's own second paragraph says the five-file list exits 0 (NOT governed), and measured on f15288e1:
$ node scripts/pm/check-governed-merges.mjs --test <the PR's five files>
governed-surface predicate: 0 of 5 path(s) hit the register … NOT governed EXIT=0
$ … --test <changeset> <high-privilege.ts> docs/adr/0090-permission-model-v2-concept-convergence.md (control)
EXIT=3
The PR's file list (git diff --name-only 482d34d60c f15288e1) is the five files, no docs/adr. The footer contradicts the body's own text and the measurement; it should be dropped or rewritten to say NOT governed. (The PR still stays draft for the merge-order reason the seat recorded — that is a separate, unaffected hold.)
N2 — non-binding (changeset wording). The table's where column reads security/index.js, index.mjs, index.d.ts, index.d.mts. Only the first carries the directory; taken literally the other three name the root-level entry files, which read 0 for declaredCapabilities (dist/index.js, index.mjs, index.d.ts, index.d.mts — all 0). All four hits are under security/. Since changeset prose ships into CHANGELOG.md, spelling security/ on each is worth one word each; the count is right either way.
Does the earlier PASS_WITH_FINDINGS on 55dc4ac6 still describe this head?
Yes. Evidence: packages/spec at f15288e1 is tree-identical to 55dc4ac6 and to a21ad008 (a3dfe3c0…), the only changed bytes are the changeset file, and the corrected prose in that file is measured true on a build whose completion is proven by the wrapper's own END rc=0 / VERDICT BUILD_OK, 34/34 declarations, and stamps equal to the recomputed source hash. F3 of 5644839802 is thereby addressed; F1 and F2 are unchanged and remain non-binding.
⛔ No label written, stripped or changed; ⛔ the PR stays draft, not enqueued, no auto-merge, no approving review; ⛔ nothing pushed. needs:contract-review on this PR and on card #17189 is the seat's to lift on this record.
Generated by Claude Code
Scoped re-review adopted — carrier lifted from both carriers. ⛔ The PR is still NOT armed, and that is a separate hold.
⭐ Tier verified by this seat from the transcript, exhaustively⇒ adoptable. ⛔ Had any row read otherwise the verdict would be void whole. The load-bearing reading, and why it is the one that mattered
The corrected counts reproduce on a build the reviewer confirmed complete before reading it (wrapper ⭐ And the replacement negative control is real in both directions: absent from built The two non-binding findings — dispositions
Carrier lifted, and what is NOT thereby cleared
⛔ This PR is still not armed, for a reason that has nothing to do with review: after the split, nothing mechanical keeps the code half behind the ADR half, and the maintainer's ordering note is verbatim 「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」 ⇒ the seat holds PR #17811 until PR #17814 is merged by hand. Landing the code first would leave a window in which this file's JSDoc describes a D5 list narrower than the published ADR still states. Generated by Claude Code |
The merge-order blocker on this PR is DISCHARGED — #17814 is merged. Two other things still stand.PM seat, DischargedThis PR's body says:
#17814 was merged by the maintainer. Verified on ⇒ the ADR-0090 D5 revision is on ⛔ Still blocking — 1: no clause-② review of record on this headBoth clause-② enqueue limbs fire here — the declaration limb (
For the record, the governed question is settled the other way and needs no human merge: ⛔ Still blocking — 2: the base is four days stale
⇒ the CI reading on Suggested order for whoever picks this up
Also still true and unchanged: this PR does not discharge #17189 — step ② (the Generated by Claude Code |
|
Tier: served at CONTRACT_REVIEW_TIER (verified by the seat from this round's transcript) Contract review (delta re-review of a moved head)
Tier verification, from this session's own transcript (located by a marker string only this session typed): 1. What the delta is — exactly one changeset file, nothing else
⇒ The delta touches changeset prose only. It moves no accept set, no source, no snapshot, no test. 2. Does the correction make the changeset TRUE? — re-measured on a clean full build, yesDetached scratch worktree of Freshness, recomputed rather than trusted: Files under
Every count reproduces. The controls the changeset now names:
⇒ The corrected paragraph is true on the tree it describes, under the build state it names. The claim it supports — the change ships, so a Non-binding, text. The table's where column elides 3. Does the existing PASS still bind to
|
Clause-② provenance — record cited, ⛔ PR deliberately NOT enqueued
Record of review, as Machine read by this seat: Tier: served at ⭐ What was actually blocking this PRAn at-tier scoped re-review of this head already existed ( ⇒ the work was reviewed; the record was invisible to the gate. A review whose heading does not match is, mechanically, no review at all. Worth a word to whoever standardises these records. The delta, verified rather than assumed
The correction itself was checked against a built artifact rather than taken on trust: clean detached worktree, offline install, build rc 0, ⛔ Why this PR is NOT being enqueuedCard #17189 is A permission-boundary change sits on the maintainer's manual floor. ⇒ this seat ⛔ does not enqueue, ⛔ does not arm auto-merge, and ⛔ does not merge. The review record exists so the merge can be made on a verified head.
Generated by Claude Code |
|
Addendum — the platform outage cleared; both PRs are now in their final states.
#17811 is deliberately left for the maintainer's hand. #17189 is
⭐ One platform fact re-confirmed today, worth keeping: a bare Generated by Claude Code |
…-predicate consumer ADR-0090 D5's `everyone`-anchor excusal for app-declared capability tokens landed as a spec predicate in PR #17811 and no consumer passed it a context, so a declared token still made an `isDefault` set unbindable at boot, at the engine write gate, at the suggestion confirm path and in the lint rule. All four now hand over `AnchorBindingContext.declaredCapabilities`, read from the stack's `capabilities:` declarations; the platform floor and the undeclared-name refusal are unchanged, and the `guest` tier is untouched. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
…, not any systemPermissions (objectstack-ai#17814) Part of objectstack-ai#17189 — the **protocol half** of phase ①. ⛔ This PR does not discharge the card, and ⛔ carries no closing keyword: objectstack-ai#17189 still owes step ②. **Sibling**: objectstack-ai#17811 carries the `packages/spec` predicate, its test, the changeset and the two regenerated surface snapshots. The two were one PR until the seat review of head `a21ad008`; they are split here on the ruling's own instruction. ## Why this is its own PR The ruling (objectstack-ai#17189 comment `5615806616`, director seat, batch objectstack-ai#110) says it twice, verbatim: > ADR-0090 D5 的 offending 清单相应收窄为「平台系统权限;带 package provenance 的应用声明 capability 令牌不计」,ADR 修订走独立受管 PR(draft、请审、人合) > `Clause-②: yes`(放宽接受集);**ADR-0090 修订单独受管 PR** The triage seat had already ruled the same shape: 「那部分**必须是独立的受管 PR**,⛔ 不得与代码同 diff」. Measured consequence, not style. `scripts/pm/check-governed-merges.mjs --test` on this PR's one-file list exits **3 = GOVERNED**; on the sibling's five-file list it exits **0 = NOT governed**. Bundled, one governed path made the predicate change human-merge-only too. Split, this half waits for its human and the code half takes ordinary landing. ## Why the revision is required Both halves are phase ①, per the ordering note (objectstack-ai#17189 comment `5617614086`) carrying the maintainer verbatim: > 「我们的项目以objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。」 D5's last bullet said **any** `systemPermissions` was an offending bit, and `describeHighPrivilegeBits` implemented that literally — so the protocol, not the implementation, was the half that was wrong. A predicate change without this revision would leave the ADR describing a rule the code no longer applies.⚠️ **Merge order.** The maintainer's sentence puts the protocol first. This PR is human-merge-only and the sibling is not, so nothing mechanical keeps the code from landing first; if that order matters to you, merge this one before objectstack-ai#17811 is enqueued. Flagging rather than deciding — a draft PR cannot enforce it. ## What changed D5's last bullet now reads "a `systemPermissions` entry naming a **platform** system permission", and names an app-declared capability token — one a package declared for itself under ADR-0066 D1, entering `sys_capability` with `managed_by: 'package'` + `package_id` provenance — as **not** an offending bit. A dated revision note records: - the two unlike token kinds in one list, and the cost that motivated the narrowing (a named downstream consumer binding its baseline set to seven positions by hand, plus one manual grant per new hire); - three boundaries, each failing closed — **the platform floor is absolute** (a platform capability name stays high-privilege however it is declared, so `manage_users` cannot be laundered by declaring it); **the discriminator is provenance, never spelling** (⛔ the dotted-name rule was considered and rejected: `setup.access` is a dotted *platform* capability today); **omission refuses** (a caller that cannot enumerate the declarations gets the pre-revision verdict); - that the D9 `guest` tier is untouched — D5 speaks for authenticated members; - the ruling's provenance and the landing order, quoted verbatim in the original Chinese; - that the consuming callers keep the pre-revision behaviour until they supply the declared list. The revision text is **byte-identical** to what stood on `a21ad008` before the split — verified by diffing this branch's file against that branch's. Removing the code hunks made no sentence of it false: nothing in the note claims the predicate ships in this PR. ## Clause ② Recorded, not re-declared here: the card's declaration is `Clause-②: yes`(放宽接受集)per the ruling's 执行 line, and the `needs:contract-review` carrier is already hung by the seat on card objectstack-ai#17189 and on the code PR objectstack-ai#17811. This half is prose only — it puts no key on any published payload and moves no accept set by itself. Whether the carrier is also owed on this PR is the seat's call; ⛔ this round writes no labels. ## Verification - Content: unchanged from the reviewed head, byte-for-byte (see above). The engineering behind it was accepted in the seat review of `a21ad008` and is ⛔ not re-litigated here. - `check-governed-merges.mjs --test docs/adr/0090-permission-model-v2-concept-convergence.md` → exit **3 (GOVERNED)**. Control, the sibling's five-file list → exit **0 (NOT governed)**, so the 3 is a verdict about this file rather than an instrument that only ever says 3. - ADR gate family on this branch: `check-adr-links`, `check-adr-symbol-anchors`, `check:adr-anchors`, `check-adr-0087-registration --base origin/main`, `check:nul-bytes`, `check:pm-governed-prose`, `check:doc-authoring` — exit codes captured before any pipe, reported in the round's report. - No changeset: `docs/adr/**` is in no package's `files[]`, and a phrase unique to this revision note occurs in 0 built files under `packages/spec/dist` — the negative control measured in the sibling PR's changeset decision.⚠️ **Governed surface** — `docs/adr/**`. Draft only, and it stays draft: ⛔ not flipped ready, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no approving review. A human merge is the review record. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ Co-authored-by: Claude <noreply@anthropic.com>
…ce-anchor predicate consumer (objectstack-ai#18602) Fixes objectstack-ai#18535 ADR-0090 D5 rules the `everyone`-anchor offending list as 「平台系统权限;带 package provenance 的应用声明 capability 令牌不计」. PR objectstack-ai#17811 landed the predicate that implements it — `describeHighPrivilegeBits(def, context?)` / `describeAnchorForbiddenBits(def, anchor, context?)`, where `AnchorBindingContext.declaredCapabilities` excuses a `systemPermissions` name, the platform floor stays absolute and an omitted context refuses — and its own changeset named this follow-up: 「the plugin-security boot refusal and the lint security-anchor-high-privilege rule pass the declared list in a follow-up」. This is that follow-up. `packages/spec/**` is untouched. ## What changed, per site Premise re-verified on the branch before editing: four consumer sites, none passing a context; `declaredCapabilities` / `AnchorBindingContext` in `packages/plugins/plugin-security/src` + `packages/lint/src` → 0 hits (control: 3 in `high-privilege.ts`). | site | before | after | |---|---|---| | `plugin-security/src/security-plugin.ts` (boot bind, `bindBaselineToEveryone`) | `const offending = boot ? describeHighPrivilegeBits(boot) : null;` | `:3595` `const offending = boot ? describeHighPrivilegeBits(boot, anchorContext) : null;` — context read once per pass at `:3592` | | `plugin-security/src/security-plugin.ts` (engine write gate) | `const offending = describeAnchorForbiddenBits(boot ?? setDef, positionName as 'everyone' \| 'guest');` | `:5503`–`:5508` the same call with `await declaredCapabilityContext()` as the third argument, memoised at `:5469` | | `plugin-security/src/suggested-audience-bindings.ts` (confirm path) | `const offending = describeAnchorForbiddenBits(setRow, row.anchor as 'everyone' \| 'guest');` | `:968`–`:972` the same call with `await readDeclaredCapabilityContext(ql, deps.metadata)` | | `lint/src/validate-security-posture.ts` (`security-anchor-high-privilege`) | `const offending = describeAnchorForbiddenBits(ps, 'everyone');` | `:795` `describeAnchorForbiddenBits(ps, 'everyone', anchorContext)`, built at `:440`–`:443` from `recordsOf(stack.capabilities)` | New module: `packages/plugins/plugin-security/src/declared-capability-context.ts` — `readDeclaredCapabilityContext(ql, metadataService)`, the registry-first / metadata-service-fallback read the `sys_capability` seeder itself uses, returning `undefined` when the stack declares nothing. ## Where the declared list is read, and why that moment is safe **Boot (the three runtime doors) reads the DECLARATIONS, not the `sys_capability` rows.** The predicate's docblock names the rows at boot; the ordering forbids it, so the card's ruled fallback applies and this is the "say so" half of it. Ordering evidence, all in `security-plugin.ts`'s `runBootstrap`: - `:3878` `for (const organizationId of catalogPasses) await bindBaselineToEveryone(organizationId);` - `:3917` `const capOutcome = await bootstrapDeclaredCapabilities(ql, this.metadata, …);` - `:3926` `await bootstrapSystemCapabilities(ql, …)` The binding runs 39 lines and one awaited pass BEFORE the seeder that writes `managed_by:'package'` rows, so on a first boot that table is empty at bind time; reading it there would refuse every declared token one layer in. The position is pinned by two other constraints stated in the code at `:3866`–`:3868`: the bind MUST follow `bootstrapBuiltinRoles` (which seeds the `everyone` anchor) and MUST precede `reconcileAudienceBindingSuggestions`. Nothing in the boot sequence was reordered. The same reader serves the engine write gate and `confirmAudienceBindingSuggestion` on purpose: the confirm check is the friendly early rendition of the gate that re-enforces the predicate on the insert it performs, so a second source there could answer "confirmed" and then have its own write refused under it. **Lint** reads the stack's own `capabilities:` collection through `recordsOf(stack.capabilities)` — the authoring-time source the predicate's docblock names, indexed by the same helper every other collection in the rule uses. No second declaration source was invented. ## Pins (each beside the consumer it guards, three cases per door) | file:line | case | |---|---| | `packages/plugins/plugin-security/src/security-plugin.test.ts:4372` | boot: a declared token BINDS (row asserted, not just a flag) | | `packages/plugins/plugin-security/src/security-plugin.test.ts:4381` | boot: an UNDECLARED token still refuses (declarations present, naming a different capability) | | `packages/plugins/plugin-security/src/security-plugin.test.ts:4392` | boot: a PLATFORM capability still refuses although the stack declares that name | | `packages/plugins/plugin-security/src/security-plugin.test.ts:4410` | write gate: admits the declared token | | `packages/plugins/plugin-security/src/security-plugin.test.ts:4415` | write gate: refuses the undeclared one — `code: PERMISSION_DENIED`, `statusCode: 403` (ADR-0112 envelope), message names the class | | `packages/plugins/plugin-security/src/security-plugin.test.ts:4426` | write gate: refuses the platform capability, same envelope | | `packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:347` | confirm: binds, and the bound row really carries the token | | `packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:365` | confirm: undeclared still refused, suggestion stays `pending` | | `packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:378` | confirm: platform capability still refused | | `packages/lint/src/validate-security-posture.test.ts:457` | lint: a declared token lints CLEAN | | `packages/lint/src/validate-security-posture.test.ts:473` | lint: an undeclared token still errors | | `packages/lint/src/validate-security-posture.test.ts:492` | lint: a platform capability still errors | The platform-floor cases reuse `high-privilege.ts`'s own vocabulary (`manage_users` from `PLATFORM_CAPABILITY_NAMES`), so the two layers cannot drift. The boot pins drive the METADATA-SERVICE door of the reader and the confirm pins drive the REGISTRY door, so both halves of the fallback are exercised. Three cases per door and not one: "the declared token binds" alone is equally satisfied by a door that stopped judging `systemPermissions` altogether. The lint meta-pins (objectstack-ai#5017) were visited deliberately rather than silenced: `stack.capabilities` joined the `stack` read surface and a `cap` receiver entry was added against `ObjectStackSchema.capabilities[]`, so the new read is held to the same "reads only keys the spec declares" rule as every other. ## Changesets - `.changeset/18535-anchor-declared-capabilities-consumers.md` — `@objectstack/plugin-security`: minor - `.changeset/18535-lint-anchor-declared-capabilities.md` — `@objectstack/lint`: minor `minor`, not `patch`: the PR declares `Clause-②: yes (widening)` and `check:changeset-no-major` requires at least one moved package at `minor` or above under that declaration. Both bodies carry the arm and the consumer-facing FROM → TO sentence. ## Measurements **Red-then-green, with the control lit.** Reverse verification ran from the COMMITTED fix, mutating the four call sites back to their pre-fix argument lists, proving the mutation reached the disk (anchored occurrence counts 1 → 0 for each fixed spelling, plus `git diff --stat`), and restoring under a `trap … EXIT INT TERM` with absolute paths. The subjects resolve through `src` (same-package relative imports), so no `dist` leg applies. - ablated `plugin-security` (both files): `Tests 3 failed | 293 passed` — exactly the three accepting pins (`binds an isDefault set …`, `binds the isDefault set …`, `write gate: admits …`) - ablated `lint`: `Tests 1 failed | 125 passed` — exactly the accepting pin - the six refusal controls (undeclared + platform, at each door) stayed GREEN under the ablation, which is what makes the four reds mean the context and not the predicate - restore leg proven by blob identity, not by an exit code: `git hash-object` of each of the three files equals its `HEAD` blob (`3a8fd520…`, `30c2ad7c…`, `f16fb00e…`), `git status` clean, `git diff HEAD` empty **Suites (merged tree, `1fcf14513`):** - `pnpm --filter @objectstack/lint --filter @objectstack/plugin-security test` → exit 0 — lint `103 files / 3868 tests`, plugin-security `113 files / 2190 tests` - `pnpm --filter @objectstack/lint --filter @objectstack/plugin-security typecheck` → exit 0, 0 `error TS` - `pnpm lint` (repo-wide `eslint . --no-inline-config`) → exit 0 — the whole population, no narrowing claimed - targeted `eslint --format json` over the 7 changed source files → 7 files, 0 errors, 0 warnings **Derived gates** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, re-derived after the merge: 71 families, all run, reconciled with `--ran` carrying each exit code → `71 derived, 68 run, 3 NOT-MEASURED, 0 UNRUN`. 67 green. The four non-zero: - `pnpm check:cross-package-test-inputs` → exit 1. NOT caused by this diff, proven with a control: at the base commit `e0d05538c` in a separate worktree the gate exits 0 with no `packages/spec/dist/` on disk, and exits 1 with the identical finding the moment one empty `packages/spec/dist/security` directory exists. The finding names `packages/cli/test/init-created-files-summary.e2e.test.ts` descending into `packages/spec/dist/` — a file this PR does not touch, in a package it does not touch. Reported for filing, not fixed here. - `pnpm check:dual-build-cjs-loads`, `pnpm check:i18n`, `pnpm check:type-check-debt` → exit 3, `PREREQUISITE NOT MET`: each refuses to measure without a full workspace build (53 packages with no `dist/`). NOT MEASURED locally, not a pass and not a finding; CI builds first and runs them for real. Three gates DID go red on this diff and were fixed, all in the new boot double: `check:engine-double-contract` (grown seam counts ratcheted with `--write`), `check:objectql-double-limit` (the `find` double now applies the caller's bound by presence, after the filter) and `check:where-matcher` (the matcher now REFUSES a `$`-prefixed combinator instead of comparing it as a field name — the refusal had to live INSIDE the matcher callback, since that gate probes the extracted matcher behaviourally). **Merge:** `origin/main` moved from `e0d05538c` to `b79fae8fb` during the work and PR objectstack-ai#18503 landed in `validate-security-posture.ts`. The one conflict was the `@objectstack/spec` import line; BOTH sides were kept (`referenceCarrierOf` from `/data` and `describeAnchorForbiddenBits, type AnchorBindingContext` from `/security`), neither dropped, and every measurement above was re-taken on the merged tree. ## Note for the contract-tier reviewer (Clause-② yes) Exactly two accept sets widen, both by the same ruled rule and both only for the `everyone` anchor: 1. the runtime anchor-binding accept set (boot bind, engine write gate, suggestion confirm) — a `systemPermissions` token THIS stack declares under `capabilities:` no longer counts as a platform system permission; 2. the lint rule `security-anchor-high-privilege`'s accept set for `isDefault: true` sets — the same names, at authoring time. What did NOT move: the platform floor (`PLATFORM_CAPABILITY_NAMES` is applied inside the predicate, so declaring `manage_users` launders nothing); undeclared names (still refused everywhere); the `guest` tier (the predicate drops the context for `guest` by contract, and no call site overrides that); the VAMA / delete / transfer / bulk-export / wildcard arms of the predicate; the boot sequence's order; and the failure direction when the declarations cannot be read — an unreadable registry, an unreadable metadata service, or an empty list all yield `undefined`, which is the pre-objectstack-ai#17811 verdict verbatim. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ility rule in objectstack-data security.md (objectstack-ai#18531) Fixes objectstack-ai#17359 **Status 2026-09-17 — the paragraph below is historical and discharged**: the follow-up it names landed as objectstack-ai#18535 (PR objectstack-ai#18602, `origin/main` `21b7c12b4`), the ceiling was ruled, and this branch merged `origin/main`; see "Patch round" at the end. ⛔ **Blocked-by: the objectstack-ai#17189 step-② follow-up (the `plugin-security` boot refusal and the `packages/lint` `security-anchor-high-privilege` rule passing the declared-capability list) — not landed on `origin/main` `2496415`, and no open card names it.** Do not land this PR before that follow-up: today the bullet's second sentence describes the protocol (`packages/spec/src/security/high-privilege.ts` + the ADR-0090 D5 revision, landed by PR objectstack-ai#17814 and PR objectstack-ai#17811) and not yet the running lint/boot — see "Premise check" below. ## What One 3-line bullet in `skills/objectstack-data/rules/security.md`, inserted after the `Source:` line of "## Object-level permissions (RBAC)" — the construct accepted on the card (assessment 5616225081, ACCEPT 5616301504) with the ACCEPT's boot-wording correction applied (「fails lint and boot」 overstated the boot side; boot refuses the binding with a warning, it does not fail). Every other line of the file is byte-identical (`git diff --numstat` = `3 0`); `skills/objectstack-platform/SKILL.md` unchanged; no other file. Lines 35–37, widths 81 / 82 / 91 characters (the file's widest line is 93): ```markdown - **`isDefault: true` = the `everyone` baseline (ADR-0090 D5).** It may carry app capabilities declared under `capabilities:` (`defineCapability`) and granted via `systemPermissions`; lint and boot refuse a platform capability or undeclared name there. ``` Wording deviation, declared: the dispatch's suggested passive form 「is refused by lint and at boot」 puts line 3 at 101 characters, over the 93 cap; the active form above keeps every noun of the accepted text and the ACCEPT's own verb ("refuse") at 91. No other word moved. ## Premise check on `origin/main` `2496415` - **A1 holds**: `security.md` is 211 lines, widest line 93, lines 31–34 verbatim as quoted in the assessment. Its last touch is `7d350a4` (objectstack-ai#17476, the Multi-tenancy section), not `6a3bcd8` as the dispatch read; the neighbours are unaffected. - **A2 holds in substance**: `git grep -F` over `skills/**` at `2496415` — `defineCapability` 0, `systemPermissions` 0, `isDefault` 1 (a list-view example), `capabilities:` 5 — and all five are the data-hook VM tokens (`capabilities: ['api.read', …]` in `objectstack-data/references/data-hooks.md` ×4 and `objectstack-ui/rules/actions.md` ×1), a third registry, not the ADR-0066 D1 stack key. Positive control `definePermissionSet` = 5 hits. The bullet duplicates nothing. - **A3 holds, and the boot side is narrower than the dispatch assumed**: `bindBaselineToEveryone` (`packages/plugins/plugin-security/src/security-plugin.ts:3581`) logs `ctx.logger.warn('[security] refusing to bind fallback set to everyone — high-privilege bits', { set, offending })` and `continue`s — a warning and no binding, never a failure.⚠️ Its call is `describeHighPrivilegeBits(boot)` (:3585) with NO `AnchorBindingContext`, and so are the other three consumers: `security-plugin.ts:5475`, `suggested-audience-bindings.ts:961`, `packages/lint/src/validate-security-posture.ts:771`. The predicate's own contract (`high-privilege.ts:134`): 「Omission refuses」. PR objectstack-ai#17811's changeset says it in so many words: 「No shipped behaviour moves in this release. Every current caller invokes the predicates with the old arity … The `@objectstack/plugin-security` boot refusal and the `@objectstack/lint` `security-anchor-high-privilege` rule pass the declared list in a follow-up」. ⇒ Today a DECLARED app token on the `isDefault: true` set is still refused by lint (error) and at boot (warn, no binding) — the shape hotclm hit. The bullet is correct for the protocol and premature for the runtime; per 「文档应该以实际实现为准」 this PR waits for the follow-up. The 「or undeclared name」 clause stays: the implicit placeholder derivation in `bootstrap-declared-capabilities.ts` is alive, and `describeHighPrivilegeBits` excuses only names on `context.declaredCapabilities`, platform floor absolute. - **Keys and anchor, all present**: `capabilities` on `ObjectStackDefinitionSchema` (`packages/spec/src/stack.zod.ts:467`, `.describe('[ADR-0066 D1] …')`); `defineCapability` (`packages/spec/src/security/capabilities.ts:214`); `systemPermissions` on `PermissionSetSchema` (`packages/spec/src/security/permission.zod.ts:691`, schema at :587); `isDefault` at :682 with `.describe('[ADR-0090 D5] App baseline for the everyone position …')`; ADR-0090 D5 at `docs/adr/0090-permission-model-v2-concept-convergence.md:46`, its objectstack-ai#17189 revision block at :249. Control: a nonexistent key greps 0 in `permission.zod.ts`. ## Line readings (the `skills/**` rule) | reading | before (`2496415`) | after (`4ea43892e`) | net | |:--|--:|--:|--:| | `skills/objectstack-data/rules/security.md` | 211 | 214 | +3 | | package: every `SKILL.md` under `skills/` (10) + `skills/objectstack-data/{rules,references}/*` (10) — 20 files | 8959 | 8962 | +3 | Token reading (the sister gate `scripts/check-skills-token-ratchet.mjs`, convention ceil(utf8 bytes / 4)): `security.md` is **2543** tokens against a ceiling of **2480** (`CEILINGS` row at `scripts/check-skills-token-ratchet.mjs:424`, pinned at the landed count with zero headroom) — **over by 63, the gate is RED at this head**. Verdict line, verbatim: 「✗ check-skills-token-ratchet: skills/objectstack-data/rules/security.md is 2543 tokens; the ratchet ceiling is 2480 (over by 63). … The other direction lands only in a PR whose body quotes a maintainer ruling authorizing it. ⛔ MAINTAINER-ONLY」. Not raised here and not paid by deletion (the accepted construct pins every other line of the file). Landing needs that one row moved to 2543 under the maintainer's word, or an equivalent deletion in the same file directed by the PM. ## Changeset `skip-changeset`, by measurement: a walk over every tracked `package.json` finds **0** manifests whose `files[]` names a `skills` path (positive control: 70 manifests name `dist`); no `skills/*/package.json` exists; the catalog ships from the GitHub tree by `npx skills add objectstack-ai/objectstack/skills` (`skills/README.md`; `packages/create-objectstack/src/skills-install.ts:62` `SKILLS_CATALOG = 'objectstack-ai/objectstack/skills'`), never inside an npm tarball. `.github/workflows/pr-automation.yml:758` lists `skills/` among the releases-nothing paths. ## Gates (local, at `4ea43892e`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 22 commands; every one was run with redirect-then-`$?` capture; `--ran` reconcile: 「22 derived, 22 run, 0 NOT-MEASURED, 0 UNRUN」, exit 0. - 21 × exit 0 — among them `check:skill-frame-sync`, `check:skill-identifier-liveness`, `check:skill-compatibility`, `check:nul-bytes`, `check:doc-authoring`, `check:role-word`, `check:corpus-claim-drift`, `check:cross-package-test-inputs`, `check:pm-governed-merges`, `check-skills-token-ratchet --self-test`. - `node scripts/check-skills-token-ratchet.mjs` → **exit 1** (the +63 above). - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` → first run exit 3 `PREREQUISITE NOT MET` (`@objectstack/formula` and `@objectstack/lint` unbuilt — not a measurement); built both under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`, held 142s), re-run → exit 0. - Not applicable: the 14 pending-changeset families (no changeset, by the measurement above); the 2 workflow-valued families and the 1 path-scheduled CI job are CI's own. - No ① dependency-closure build and no ② package test: the diff touches no package. - Control-character self-scan of the file: none. ## Acceptance notes - noted, not filed — 承接者: the PM, relayed in the dev report as a class (b) contract finding with dedupe words: ADR-0090 D5 [ruled] and `describeHighPrivilegeBits` say a declared app token is not an offending bit, while the four consumers listed under A3 still refuse it. PR objectstack-ai#17811's changeset promises the follow-up; no open card in `domain:services` (48), `domain:devx` (103), `domain:spec` (144), `security` (19) or `finding` (78) names it (lists read 2026-09-16, keyword set `17189 / bindBaselineToEveryone / declaredCapabilities / AnchorBindingContext / security-anchor-high-privilege`). - noted, not filed — 承接者: 无: with this bullet the word "capabilities" names three registries across the published bundle (data-hook VM tokens, `requires:` platform service tokens, ADR-0066 D1 stack declarations). Not a defect; the assessment placed the stack key beside its GRANT key on purpose. ## 维护者速读(草稿) **改了什么**:在 `skills/objectstack-data/rules/security.md` 的「Object-level permissions (RBAC)」键列表里加一条三行要点:`isDefault: true` 的权限集就是 `everyone` 基线(ADR-0090 D5);它可以携带本应用在 `capabilities:` 下用 `defineCapability` 声明、再经 `systemPermissions` 授予的应用能力;平台能力或未声明的名字放在那里会被 lint 与启动拒绝。文件其余各行一字未动,`skills/objectstack-platform/SKILL.md` 不动。 2026-09-17 补丁轮:合入 `origin/main`(合并提交 `68e1b07e1`;objectstack-ai#18535 已落地,这三行描述的 lint 与启动行为已成真,三行本身一字未改),并按维护者裁定把 `scripts/check-skills-token-ratchet.mjs` 里 `security.md` 的 token 上限行从 2480 抬到 2543,上限行旁按该文件自己的抬限格式逐字引用裁定「security.md 允许增加到 2543」;合并后实测恰为 2543(`ceil(utf8 bytes / 4)`),余量 0,其它上限行不动。 **为什么改**:objectstack-ai#17189 裁定 (i) 之后,「默认权限集携带本应用自己的门牌令牌」这一组合从被拒变为合法(前提是先声明),而已发布的 skills 里没有任何一处写到这三个键;AI 作者照 schema 直接写 `systemPermissions` 就会写出 hotclm 踩过的那种形状。已接受的评估(5616225081)裁定只加这一条、不加反例、`+3` 行。 **风险与代价(含回滚)**:① 时序——objectstack-ai#17811 只落了协议这一半(spec 谓词 + ADR 修订),启动侧与 lint 侧尚未把声明清单传给谓词,今天照这条要点写出的默认集仍会被 lint 报错、启动只警告不绑定;所以本 PR ⛔ 不应先于那一半落地(正文顶部已标 Blocked-by)。② 已发布 skills 的 token 棘轮:`security.md` 上限 2480、余量 0,本次 +63 使 `check-skills-token-ratchet` 变红;上限行(`scripts/check-skills-token-ratchet.mjs:424`)按门禁自述只有维护者裁定可抬,本 PR 未抬。回滚 = revert 这一个提交(单文件 +3 行,无发布物)。 2026-09-17 更新:① 已解除——objectstack-ai#18535 落地,启动侧与 lint 侧都已把声明清单传给谓词;② 已解除——维护者裁定抬到 2543,本 PR 抬行,`check-skills-token-ratchet` 在新 head 上为绿。回滚 = revert 两个非合并提交(`4ea43892` 三行 + `dfe355143` 上限行),仍无发布物。 **席位意见**: **你要做的**:① 确认 objectstack-ai#17189 第 ② 步(`plugin-security` 启动拒绝 + `packages/lint` 规则传入声明清单)是否已有卡;没有则立卡,并让本 PR 排在它之后合并。② 决定 token 上限:把 `security.md` 那一行抬到 2543(在本 PR 正文引用你的裁定),或指示在同一文件删等量内容(这会动已接受评估钉死的其他行)。③ 之后由 skills 席四件套复核,你点合并。 2026-09-17 更新:① ② 已完成(objectstack-ai#18535 落地;上限已按你的裁定抬到 2543 并在正文与上限行旁引用),只剩 ③。 ## Patch round — merge main + ceiling raise (2026-09-17) Both park conditions discharged (park note 5704585750, update 5710537792, unpark 5711767613). The `Blocked-by:` paragraph at the top of this body is historical: objectstack-ai#18535 landed on `origin/main` as `21b7c12b4` (PR objectstack-ai#18602). Same branch, commits added on top — no rebase, no force-push, no new PR. **Merge**: `68e1b07e1` = `git merge --no-ff origin/main` (`21b7c12b4`) into the branch. Clean (`git merge-tree --write-tree` exit 0; 37 commits behind at merge time); no `os-regen-pending` recorded (the branch touches no generated artifact). Three-dot delta vs `origin/main` after the round: `skills/objectstack-data/rules/security.md` +3/−0 (byte-identical to `4ea43892`), `scripts/check-skills-token-ratchet.mjs` +10/−1. **Re-count** (the ratchet's convention, `ceil(utf8 bytes / 4)`): `security.md` on `origin/main` `21b7c12b4` = 9913 bytes → 2479 tokens (main did not touch the file since the branch forked); branch head before the round `4ea43892` = 10170 bytes → 2543; after the merge (`68e1b07e1`, and the head `dfe355143`) = 10170 bytes → **2543**. Not more than 2543, so the raise is exactly the ruling. Line readings unchanged: file 211 → 214 (+3), widest line 93, package (20 files) 8959 → 8962. **Ratchet row** (`CEILINGS` in `scripts/check-skills-token-ratchet.mjs`): - before: `['skills/objectstack-data/rules/security.md', 2480],` (line 424 on `21b7c12b4`) - after: `['skills/objectstack-data/rules/security.md', 2543],` (line 433 on `dfe355143`), with the raise recorded beside the row in the file's own raise-ritual form (before → after, the surface it authorizes, the arithmetic +63 / headroom 0 / ceiling +63, the ruling verbatim with its record id). No other row moves; the ceiling equals the measurement. **Maintainer ruling**, verbatim and untranslated (maintainer, 2026-09-17, recorded on objectstack-ai#17359 as comment 5710537499): > 「security.md 允许增加到 2543」 That is the authorization the ratchet's own rule requires: 「the other direction lands only in a PR whose body quotes a maintainer ruling authorizing it」. **Re-read of the three lines against the landed code** (`origin/main` `21b7c12b4`): | clause | verdict | evidence | |:--|:--|:--| | `isDefault: true` = the `everyone` baseline (ADR-0090 D5) | still true | `packages/spec/src/security/permission.zod.ts:682` `.describe('[ADR-0090 D5] App baseline for the everyone position …')`; ADR-0090 D5 at `docs/adr/0090-permission-model-v2-concept-convergence.md:46` | | it may carry app capabilities declared under `capabilities:` (`defineCapability`) and granted via `systemPermissions` | now true at boot and in lint (protocol-only before objectstack-ai#18535) | `packages/plugins/plugin-security/src/declared-capability-context.ts` (`readDeclaredCapabilityContext` reads the stack's `capabilities:` declarations, registry first, metadata service as fallback); boot bind `security-plugin.ts:3592–3595` `describeHighPrivilegeBits(boot, anchorContext)`; engine write gate `:5503–5508`; confirm path `suggested-audience-bindings.ts:968–972`; lint `packages/lint/src/validate-security-posture.ts:439–443` builds `anchorContext` from `stack.capabilities`, `:795` passes it to `describeAnchorForbiddenBits` | | lint and boot refuse a platform capability or undeclared name there | still true | `packages/spec/src/security/high-privilege.ts:70–74` the platform floor (`PLATFORM_CAPABILITY_NAMES` is never excused), `:134` 「Omission refuses」 — a token absent from `declaredCapabilities` stays offending; lint emits `security-anchor-high-privilege` at severity `error` (`validate-security-posture.ts:795–808`); boot logs `[security] refusing to bind fallback set to everyone — high-privilege bits` and skips the binding (`security-plugin.ts:3595–3600`) | No clause corrected; the three lines are unchanged. **Ablation of the row** (from the committed head `dfe355143`; trap-restored, absolute paths; no `dist/` involved — the gate reads its own source): with the row reverted to 2480 on disk (grep counts: 2480-row 1, 2543-row 0) the gate exits 1 — 「✗ check-skills-token-ratchet: skills/objectstack-data/rules/security.md is 2543 tokens; the ratchet ceiling is 2480 (over by 63). … ⛔ MAINTAINER-ONLY」; restored with `git checkout HEAD -- …`: `git hash-object` = HEAD blob `5b984866`, `git diff HEAD` empty, `git status --porcelain` empty, gate exits 0 again with 「is 2543 tokens (ceiling 2543; headroom 0)」. **Gates** (worktree at `dfe355143`; `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, change set derived from the merge base `21b7c12b4`, 2 paths): 40 commands derived, all 40 run with redirect-then-`$?` capture; `--ran` reconcile: 「40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN」, exit 0. `pnpm --filter @objectstack/lint run check:doc-formula-expressions` measured after building `@objectstack/lint...` under `scripts/pm/os-verify-lock.sh` (「VERDICT command-exit 0 · held the lock 136s · waited 0s」). `pnpm check:pm-dispatch-gates` took 626 s under contention. The list, byte-for-byte as derived, with exit codes: ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-scripts-symbol-anchors.mjs :: exit 0 node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-compatibility :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Named verdict lines: `check-skills-token-ratchet` 「✓ … security.md is 2543 tokens (ceiling 2543; headroom 0)」 and 「34 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted」; its `--self-test` 「65 cases pass」; `check-ratchet-remedy-authority` 「255 scripts swept … 15 mark the expanding remedy ⛔ MAINTAINER-ONLY」 (unchanged); `check-skill-frame-sync` self-test 14 cases + the frame coherent. Not owed locally: the 51 artifact-roster, 11 wide-population and 14 pending-changeset families, the path-scheduled CI job and the always-runs tail (CI's own); no ① dependency-closure build or ② package test is owed — the diff touches no package. **Governed**: `node scripts/pm/check-governed-merges.mjs --test skills/objectstack-data/rules/security.md scripts/check-skills-token-ratchet.mjs` → exit 3, 「GOVERNED — a human merge is the review record for this PR」 (1 of 2 paths hit the register; `skills/**` is the rules layer). Still draft; not for any seat to land. **Changeset**: still `skip-changeset` — `scripts/check-skills-token-ratchet.mjs` is a repo-root gate script (private root package; no `files[]` ships it), and the `skills/**` measurement above stands. Label set read back after the round: `documentation`, `size/s`, `skip-changeset` (no label written this round). **Control characters**: `grep -naP` over both touched files → no match (exit 1). --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…lity DECLARATIONS, not the not-yet-seeded sys_capability rows (objectstack-ai#18767) Clause-②: no Fixes objectstack-ai#18603 Comment text only, in one file: the `AnchorBindingContext` docblock in `packages/spec/src/security/high-privilege.ts`. No predicate, type, export or accept set moves. ## What the sentence said, and why a literal follower is refused The docblock named two sources for `declaredCapabilities`: at boot 「the `sys_capability` rows carrying `managed_by: 'package'` provenance」, at authoring time the stack's own `capabilities` array. The boot half carried an ordering precondition the sentence never stated. The ADR-0090 D5 anchor binding runs BEFORE the seeder that writes those rows, so on a first boot the table is empty at exactly the moment the docblock said to read it — and this docblock's own 「omission refuses」 property then turns that emptiness into a silent refusal of every declared token: the app's own `isDefault` set unbindable at the `everyone` anchor, which is the defect objectstack-ai#17811 introduced the input to remove, reproduced one layer in. The boot half now names the DECLARATIONS, read through the seeder's own two-step — the ObjectQL registry first, the metadata service as the fallback — which is exactly what `readDeclaredCapabilityContext` (`@objectstack/plugin-security`, PR objectstack-ai#18602) already implements, so the contract text and its one runtime consumer corroborate each other instead of contradicting. The `sys_capability` rows stay a valid source, qualified: only once the seeder has written them. ## LIT — the ordering was READ, by symbol, on this branch's base The card's line numbers were taken on PR objectstack-ai#18602's head and were carried forward unverified. They were re-derived here by SYMBOL on `origin/main` `95b21b33be` (this branch's merge base), `packages/plugins/plugin-security/src/security-plugin.ts`: | symbol | line | inside | | :-- | :-- | :-- | | `const runBootstrap` | `:3655` | the boot sweep itself | | `await seedCatalogBuiltins(...)` | `:3866` | `runBootstrap` — reaches `bootstrapBuiltinRoles` at `:3572` (defined in `seedCatalogBuiltins`, `:3570`), which seeds the `everyone` anchor | | `await bindBaselineToEveryone(...)` | `:3888` | `runBootstrap` — the ADR-0090 D5 bind; defined at `:3583`, consults `describeHighPrivilegeBits` at `:3595` | | `await reconcileAudienceBindingSuggestions(...)` | `:3905` | `runBootstrap` | | `await bootstrapDeclaredCapabilities(...)` | `:3927` | `runBootstrap` — the seeder that WRITES the `managed_by: 'package'` rows | `:3888` and `:3927` sit in one straight-line `try` body of one function with no branch between them, so the bind precedes the seeder. **The card's conclusion holds.** Three line attributions in the card's table are worth correcting for the next reader, and none of them moves the conclusion: - `:3572` is `bootstrapBuiltinRoles`'s call site inside the helper `seedCatalogBuiltins` (`:3570`), not a line of `runBootstrap`; `runBootstrap` reaches it at `:3866`. - `:3639` is a SECOND `bindBaselineToEveryone` call, inside `seedCatalogForOrganization` (`:3635`) — the organization-creation hook, not the boot sweep. Only `:3888` is `runBootstrap`'s. - `:3742` is `reconcileAudienceBindingSuggestions` inside the publish-materializer callback `runBootstrap` registers — a runtime publish path, not a boot step. The boot step is `:3905`. ## DARK — a reading that must be ZERO, with a control proving it fires Predicate: take `git diff -U0` over `packages/spec/src/security/high-privilege.ts`, keep the `+`/`-` lines that are not the `+++`/`---` headers, and drop every one that is blank or begins with `*`, `//` or `/*`. What remains is CODE. | leg | input | reading | | :-- | :-- | :-- | | this change | `git diff -U0 95b21b3 HEAD -- packages/spec/src/security/high-privilege.ts` | `NON_COMMENT_CHANGED_LINES=0` | | control | the same file's own `d5c91dd681` (objectstack-ai#17811), same predicate, same input shape | `NON_COMMENT_CHANGED_LINES=33` — it names the added `import`, the `export interface AnchorBindingContext`, its member and the whole of `appDeclaredCapabilityNames` | The zero is a measurement, not an absence of input: the same instrument reads 33 on a real code change to the same file. `git diff --stat` for this change is 17 insertions / 2 deletions, all of them comment. ## Changeset — measured, not assumed `skip-changeset` would be wrong: published content moves. - `packages/spec/src/security/high-privilege.ts` is NOT shipped as source. `@objectstack/spec`'s `files[]` takes `src/**/*.zod.ts` and this file is not one — `npm pack --dry-run --json` lists 2021 shipped paths and does not include it, with the sibling `src/security/permission.zod.ts` present in the same listing as the lit control. - Its published reach is the EMITTED declarations, and they move. After `pnpm --filter @objectstack/spec build`, the new clause is present in `dist/security/index.d.ts` and `dist/security/index.d.mts` — both in that same shipped listing — the superseded spelling is absent from every built declaration file (0 files), and the docblock's unchanged neighbouring sentence (「Never synthesize this from the set under test」) is present in the same two files as the lit control. Hence `.changeset/18603-anchor-binding-declared-capabilities.md`, `@objectstack/spec: patch`. ## Verification, at `2387ad9a5c` - `pnpm --filter @objectstack/spec build` — green. - `pnpm --filter @objectstack/spec test` — 486 test files, 14017 tests, all passed. - `pnpm --filter @objectstack/spec typecheck` — green. - `pnpm --filter @objectstack/spec check:generated` — all 15 generated artifacts up to date; nothing needed regenerating. - `pnpm build` — 73/73 tasks successful. - `pnpm lint` (`eslint . --no-inline-config`, the repo-wide population) — green, exit 0. - The gate families derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: all 75 run, all exit 0, reconciled with `--ran` (75 derived / 75 run / 0 NOT-MEASURED, derived from recorded exit codes). Three of them (`check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`) first answered `exit 3` PREREQUISITE NOT MET on an unbuilt tree, which is not a finding; they were re-run green after `pnpm build`. ## Acceptance notes Nothing filable was found alongside this change. The three line-attribution corrections above are reported here rather than filed: they are a nuance in a card's evidence table, not a defect in the code, and the ordering they describe is correct. Landing is the owning seat's — left as a draft, auto-merge not armed. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #17189 — step ① of the ruled order only. ⛔ This PR does not discharge the card and ⛔ carries no closing keyword: the
plugin-securityboot refusal and the@objectstack/lintsecurity-anchor-high-privilegerule are step ②, deliberately untouched here.Sibling: #17814 carries the ADR-0090 D5 revision alone, as its own governed draft PR. The two were one PR until the seat review of head
a21ad008; they are split on the ruling's own instruction — 「ADR-0090 修订单独受管 PR」 and 「ADR 修订走独立受管 PR(draft、请审、人合)」 (#17189 comment5615806616), which the triage seat had already spelled 「⛔ 不得与代码同 diff」. Measured:check-governed-merges --teston this PR's five-file list exits 0 (NOT governed); add the ADR back and the same predicate exits 3 (GOVERNED). Bundled, one governed path made the predicate change human-merge-only too. ⭐ Both halves are phase ① and ⛔ neither is dropped.5617614086) puts the protocol first: 「协议不正确的应该先修改协议。」 The ADR half is human-merge-only and this half is not, so nothing mechanical keeps this one from landing first. If that order matters, merge #17814 before this is enqueued — flagging rather than deciding, since a draft PR cannot enforce it.capabilitywithscope: 'org'counts as a high-privilege bit, so a set carrying only app capability tokens cannot bind to theeveryoneanchor #17189 comment5615806616) declared this value in advance, in its 执行 line, and a declaration made in advance is not overruled by a reading of the diff. My earliernoapplied a narrower test ("no new key on a published payload") than clause ② asks. The diff agrees with the ruling three ways: a permission set that was refused is now accepted;packages/spec/api-surface/security.jsonandexport-origins/security.jsonwere regenerated, so the published-surface snapshots moved; and the changeset's own rationale says "a widened accept set".The defect
describeHighPrivilegeBitscounted any non-emptysystemPermissionsas a high-privilege bit, so a permission set carrying the capability token its own app declared could not be bound to theeveryoneaudience anchor.Re-confirmed on this branch's own base (
482d34d60c) against a freshly builtdist, with a lit control:{ objects: { a: { allowRead: true } } }and{ systemPermissions: [] }both returnednullin the same run, so the instrument could have come back the other way. The shipped predicate's arity was1— there was no channel through which the distinction could have arrived.The change
Both predicates take a new optional
AnchorBindingContextnaming the capability names this stack declared (ADR-0066 D1:defineCapability, enteringsys_capabilitywithmanaged_by: 'package'+package_idprovenance). A token on that list is the app's own gate and is not counted as a system permission.The discriminator is provenance, not spelling, and that is the point rather than a convenience. The rejected alternative was a naming-syntax rule (dotted ⇒ app token). It misjudges in silence in both directions:
setup.accessis a platform capability that is dotted today, and nothing stops an app declaring an undotted token. A syntax rule guesses; the declared list is a fact the caller can read, and only the caller can read it — the predicate is pure and synchronous by contract, and a set may never vouch for its own tokens.Two properties keep the widening honest, both fail-closed:
PLATFORM_CAPABILITY_NAMESstays high-privilege however it is declared, so a package cannot laundermanage_userspast the gate by declaring a capability of that name.guest(ADR-0090 D9) does not honour the excusal at all: D5 speaks for authenticated members, and conferring an app's own gate on anonymous visitors is a different act that ruling (i) did not decide.Both directions tested
packages/spec/src/security/high-privilege.test.ts, 15 cases. Newly accepted: a set whose only system permission is a declared token, in the authored shape, thesys_permission_setJSON-string column shape, declaration/registry row entries, and the filing consumer's real shape. Still refused: a platform permission even when a package declares that name;setup.access— dotted, declared, still refused; any undeclared token; every token when no list is passed; a mixed set (one unexcused token refuses the whole set); non-string entries; every other D5 bit (VAMA, delete/transfer, bulk export); and the wholeguesttier.Ablation (both legs: mutate, prove it reached disk with an occurrence count on the mutated text, run, restore; restore verified byte-identical to the
HEADblob withgit diff HEADempty):unexcused = sys)The first leg reddened one test more than predicted:
leaves every other D5 bit refusingcarries both a declared token and an object bit, so without the excusal thesystemPermissionsbranch answers before the object branch is reached. Reported as observed, not as predicted.ADR-0090 D5
The revision is not optional here. D5's last bullet said any
systemPermissions, and the code implemented that literally — so the protocol, not the implementation, was the thing that was wrong. Per the maintainer's ordering, verbatim:The bullet now reads "a⚠️ Until #17814 merges, this file's own JSDoc describes a D5 list narrower than the published ADR still states; that window is the split's cost, and the merge-order note above is how to close it.
systemPermissionsentry naming a platform system permission", with a dated revision note recording the two token kinds, the three fail-closed boundaries, why the spelling rule was rejected, thatguestis untouched, and that the callers keep the pre-revision behaviour until they supply the list. That change now lives in #17814, byte-identical to what stood here ona21ad008— verified by diffing the two branches' copies of the file. Removing the code hunks from it made no sentence of it false: nothing in the note ever claimed the predicate ships alongside it.Verification
pnpm --filter @objectstack/spec test— 472 files / 13365 tests passed.pnpm --filter @objectstack/spec typecheck— passed (tsc --noEmit, scripts, and the test-layer ledger; the new test file compiles clean and is not added totest-typecheck-debt.json).plugin-securityaudience-anchors.test.ts+audience-anchor-set-claims.pin.test.ts(20 passed — including the pin that reads this very JSDoc block),@objectstack/lintvalidate-security-posture*.test.ts(139 passed).scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 83 commands — 79 exit 0, and four exit 3 = PREREQUISITE NOT MET (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt,check:doc-formula-expressions). All four read a whole-repo build; the closure build was OOM-killed on this shared box at 11m52s. They are NOT MEASURED, ⛔ not green, and are declared to CI. Exactly six commands left this PR's family when the ADR did —check-adr-links,check-adr-symbol-anchors(each with its--self-test),check:adr-anchorsandcheck:pm-governed-merges— and all six are run green on docs(adr): ADR-0090 D5 offending bits are platform system permissions, not any systemPermissions #17814 instead, which derives 18 commands of its own (17 green, the samecheck:doc-formula-expressionsat 3).git diff a21ad008 HEAD -- packages/specis empty. Only the ADR file and one changeset sentence moved (the sentence said the revision landed "in the same PR", which the split made false; it now names docs(adr): ADR-0090 D5 offending bits are platform system permissions, not any systemPermissions #17814).eslint . --no-inline-configover the whole repo — exit 0, measured ona21ad008, whosepackages/spectree is byte-identical to this head. No narrowing claimed.api-surface/security.json,export-origins/security.json(one line each, the new interface).check:generatedgreen.dist, thenpnpm --filter @objectstack/spec buildwith both passes, exit 0,check-dts-emitted34/34, and bothdist/.build-input-hash*matchingsrc.declaredCapabilitiesreaches 4 files (security/index.js,index.mjs,index.d.ts,index.d.mts) and the positive controldescribeHighPrivilegeBits4;AnchorBindingContext2 (declarations only - it is a type) andappDeclaredCapabilityNames2 (JS only - it is module-private). Negative control: a sentence occurring only in the ADR revision reads 0, anddocs/adr/**is in no package'sfiles[].The platform floor is absoluteis in this predicate's own JSDoc as well as in the ADR, so it was never ADR-unique and reads 2, not 0, once declarations exist. Both the number and the control are corrected, and the changeset now names the build state, because the build state is what changes the answer. The conclusion it supports - that a changeset is owed - never moved.git diffoverpackages/plugins/plugin-securityandpackages/lintagainst the merge base is empty.维护者速读(草稿)
改了什么 — 应用自己声明的「门牌」不再被当成平台系统权限。应用现在可以把「全体员工都持有」的权限集绑到
everyone,即使这个集合带着它自己导航要读的那张门牌。平台权限(manage_users一类)的保护一点没松。本 PR 同时按裁决修订了 ADR-0090 D5 的清单——协议先改,实现跟上。为什么改 — 具名下游
objectstack-ai/hotclm被这条规则挡住:它无法表达「全体员工」,只能把权限集逐一绑到七个岗位,再由管理员为每个没有岗位的员工手工授予;每一个新入职都是一次手工步骤,永远。该仓维护者已裁定保留这个 workaround 等本修。风险与代价(含回滚) — 本次发布没有任何已有行为变化:所有调用方仍用旧参数调用,不传名单时判定与改前逐字一致。风险集中在第二步(
plugin-security与lint开始传名单)落地时,而不是现在。误判方向是「多拒」不是「多放」:名单缺失即拒绝。平台权限有绝对下限——应用声明一个叫manage_users的能力也洗不白它。guest(匿名访客)这一档完全不放宽。回滚成本低:本 PR 是一个可选参数加一份文档修订,git revert即可,无数据迁移、无存储格式变化。席位意见 — (留给维护者)
你要做的 — 一、确认 D5 修订的措辞就是您要的协议(这是受管面,需要您人工合并)。二、确认「应用声明过的能力可以发给全体员工」这条安全姿态判断——本 PR 只按已声明的出处区分,不按
scope: 'org'再收窄一层;若您要求更窄的判据,说一声,第二步的调用方过滤一下名单即可,谓词不用改。docs/adr/0090-…,check-governed-merges --teston its file list exited 3 (GOVERNED). It no longer carries it: re-measured on the five-file list at 2026-09-12T09:5xZ the same predicate exits 0 — NOT governed, with the ADR appended as the control exiting 3. ⇒ ordinary queue landing applies to this PR, and the human-merge rule applies to #17814 instead. ⛔ Still not armed, for a different reason: the maintainer's ordering note puts the protocol first, so the seat holds this PR until #17814 is merged by hand.Generated by Claude Code
Generated by Claude Code