Skip to content

Commit 92fd343

Browse files
docs(permissions): name where a high-privilege anchor binding is refused; spec patch changeset for the anchor docblock (#24006)
Fixes #23887 Fixes #23935 Clause-②: no ## What The contract review of the anchor-docblock change (`6f7ad7900f`, review `6214176883` on #23373) left two release-text items. This PR handles both: 1. **`.changeset/23887-anchor-docblock-guest-no-door.md`**: `'@objectstack/spec': patch` for the docblock that change landed on `EVERYONE_POSITION` / `GUEST_POSITION` (`packages/spec/src/identity/position.zod.ts:159-167`). The docblock now says packages suggest bindings to `everyone` only and the admin confirms each one, and that no door binds a set to `guest` until ADR-0138 D3 item 5's bind door is built. 2. **`content/docs/permissions/authorization.mdx:68-78`**: the page said "The `everyone`/`guest` audience-anchor bindings reject high-privilege sets at the data layer for every caller". That stopped being true when the data-layer gate retired with `sys_position_permission_set`. The new text names each refusal point measured below. This PR changes no code, no other docs page and nothing under `packages/spec/src/**`. It touches no governed surface. ## Why the changeset is owed (measured on this tree's build) - `packages/spec/package.json` `files[]` = `["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json","protocol-upgrade-guide.md"]`. So `src/identity/position.zod.ts` ships in the tarball. - After `pnpm --filter @objectstack/spec build`, the new sentence (``No door binds a set to `guest` ``) appears in `dist/membership-role-BpDGLF1o.d.ts` and `dist/membership-role-a9eypi2I.d.mts`. Positive control: the older sibling sentence ("its bindings face the strictest lint tier") appears in the same two files. ## Where a high-privilege set bound to an anchor is refused (measured) Tree: `packages/**` byte-identical to `origin/main` `5fb8d158b5`. Search: `git grep -n describeAnchorForbiddenBits` gives 38 hits. Only 3 are production call sites, and all 3 pass `everyone`. The other production anchor check is the one `describeHighPrivilegeBits` caller in `security-plugin.ts`. The anchor seal paths were read separately. The line numbers the card gave (`:512`, `:865`) have since moved to `:659` and `:1040`. | # | Refusal point | Code | Answer | Measured by | |---|---|---|---|---| | 1 | Accepting a package `isDefault` suggestion: `POST /api/v1/security/suggested-bindings/:id/confirm` calls `confirmAudienceBindingSuggestion` | `plugin-security/src/suggested-audience-bindings.ts:1040` (`everyone` tier, with the stack's declared capabilities) | `403 PERMISSION_DENIED`; the row stays `pending` and nothing is recorded | `suggested-audience-bindings.test.ts` "refuses a set carrying anchor-forbidden bits — 403, nothing recorded", plus the undeclared-token and platform-capability controls: passed | | 2 | Each derivation of `everyone`'s definition, acceptance half: `readAcceptedEveryoneSets`, for record rows and for a walled host's `acceptedDefaultPermissionSets` | `suggested-audience-bindings.ts:659` | the set is withheld and reported as `forbidden`; `declareEveryone` logs it at `error` | tests "withholds an accepted set whose published body now carries a forbidden bit" and "a declared name passes the everyone anchor's high-risk gate": passed | | 3 | Each derivation, baseline half: `everyoneBaseline` (`describeHighPrivilegeBits`, which is the whole `everyone` tier) | `plugin-security/src/security-plugin.ts:4755-4765`, called from `declareBuiltinPositions` (`init()`) and `declareEveryone` (`kernel:ready`, and `rederiveEveryone` on accept, revoke, uninstall and package publish) | the baseline set is left off `everyone`, with a `warn` | `builtin-positions.declaration-phase.test.ts` and the `security-plugin.test.ts` block "audience-anchor bindings read the stack's declared capabilities": passed | | 4 | Author time: `security-anchor-high-privilege` in `validateSecurityPosture`, keyed on `isDefault: true` only | `lint/src/validate-security-posture.ts:940`; registry row `lint/src/authoring-rules.ts:2010-2019` (`gating`, commands `validate`/`build`/`lint`, surfaces `cli` + `runtime-publish`, `runtimeTypes` include `permission`) | an `error` finding on the CLI. At the metadata door's publish it is `422 INVALID_METADATA` (`metadata-protocol/src/runtime-authoring-gate.ts:1194-1195`). Draft saves are not gated | scratch probe below and `validate-security-posture.test.ts` (166 passed) | | 5 | `guest` | no production caller of the `guest` tier | no door binds a set: the confirm door refuses a `guest` row with `409 SUGGESTION_STATE` (`suggested-audience-bindings.ts:1009`), and the sync never produces one | test "refuses a guest-anchor row loudly rather than record a grant it does not deliver": passed | | 6 | The anchor seal (any set, high-privilege or not) | package door: `objectql/src/security-catalog-namespace.ts:122` `BUILT_IN_SECURITY_CATALOG_NAMES`; metadata door: the sealed-managed-item refusal at `metadata-protocol/src/protocol.ts:18279` | package door: refused. Metadata door: `403 NOT_OVERRIDABLE` | `registry-security-catalog-namespace.test.ts` "the built-in holders" (9 passed, `position everyone` among them). The metadata-door half is pinned for `guest` by `packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts:479-491`, which I read and did not run (it needs the whole-workspace dogfood boot). `everyone` goes through the same predicate, which I also read and did not run | No engine (data-layer) write gate on anchor bindings remains: the plugin-security write gates mention no anchor refusal. `delegated-admin-gate.ts` `ANCHOR_POSITIONS` refuses `sys_user_position` assignments to an anchor, which is a user assignment and not a set binding. The `/security/_activation` door refuses switching an anchor (`400`), which is not a binding either. **Scratch probe** (not committed; run with `tsx` against `packages/lint/src` over the built closure). Calls `runRuntimeAuthoringRules({ type: 'permission', item })` and `validateSecurityPosture({ permissions: [item] })`: ```text runtime rules for permission: validateRetiredPermissionResidue,validateSecurityPosture,validateSecurityRoleWord,validateRlsPredicateEnforceability isDefault_with_delete {"runtimeErrors":["security-anchor-high-privilege"],"runtimeAdvisories":[],"cliFindings":["security-anchor-high-privilege:error"]} isDefault_read_only {"runtimeErrors":[],"runtimeAdvisories":[],"cliFindings":[]} not_default_with_delete {"runtimeErrors":[],"runtimeAdvisories":[],"cliFindings":[]} ``` **Tests run locally** (each through `scripts/pm/os-verify-lock.sh`, every `VERDICT command-exit 0`, after building the plugin-security and lint dependency closures): - `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/suggested-audience-bindings.test.ts src/builtin-positions.declaration-phase.test.ts src/audience-anchors.test.ts`: 3 files, 65 passed - `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/security-plugin.test.ts -t 'audience-anchor bindings read the stack'`: 3 passed, 217 skipped - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-security-posture.test.ts`: 166 passed - `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/registry-security-catalog-namespace.test.ts -t 'built-in holders'`: 9 passed, 22 skipped ## Gates (head `240caf9b61`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 55 commands for this diff. All 55 exited 0. One of them, `pnpm --filter @objectstack/spec run check:skill-examples`, first exited 3 (PREREQUISITE NOT MET, no `client-react` declarations). I built `@objectstack/client-react` and its closure, and the rerun exited 0 with 263 examples type-checked. - `dispatch-gates --ran` gave: 55 derived, 55 run, 0 NOT-MEASURED, 0 UNRUN. - Changeset family: - `check-empty-changeset`: "1 declaring changeset(s) added" - `check-adr-0087-registration`: "no declared-breaking changeset (1 non-breaking changeset(s) seen)" - `check-changeset-no-major`: protocol lockstep holds. Its clause-② level axis prints NOT APPLICABLE locally (no `pull_request` payload), so this PR's `Check Changeset` run reads that axis from the line above. - Docs: - `check:doc-anchors`: 531 fragment links resolve - `check:doc-authoring`: clean - `check:docs-transcript-drift`: clean - `check:docs-audit-scope`: clean - `check:nul-bytes`: OK - Run outside the derived list: - `node scripts/docs-audit/check-affected-docs.mjs` exits 0, and `node scripts/docs-audit/affected-docs.mjs origin/main` reports "0 docs name something this change touched … 0 changed package(s)". The Docs Drift Check workflow triggers on `packages/**` only, so it does not run on this PR. - `node scripts/check-adr-links.mjs`: 765 links resolve. Lychee runs in CI. ## Acceptance notes - The changeset will be collected by the next Version Packages PR as though it had shipped with the docblock change. - NOT MEASURED locally: the metadata-door `403 NOT_OVERRIDABLE` for the anchors (row 6), pinned by dogfood that only main's full battery runs. Also not run: `check:dev-prereqs` and `check:skill-frame-freshness` (local-only families; this diff edits no `packages/spec` source and no four-axis framework file). - No label written: the dispatch named none, and `skip-changeset` does not apply. --- _Generated by [Claude Code](https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bbdfc05 commit 92fd343

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Clause-②: no
6+
7+
The docblock on `EVERYONE_POSITION` and `GUEST_POSITION` no longer says that packages suggest
8+
bindings to both audience anchors. It now reads that packages suggest bindings to `everyone` at
9+
install time and the admin confirms each one, never auto-bound, and that no door binds a
10+
permission set to `guest` until ADR-0138 D3 item 5's bind door is built, an act of the
11+
environment's administrator and never of a package. The docblock ships in the package's
12+
`src/**/*.zod.ts` files and in the JSDoc of `dist/*.d.ts`, so it is the hover text an author or an
13+
AI reads on the two constants. No key, schema, export or accepted value changes.

‎content/docs/permissions/authorization.mdx‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ session from protected resources while keeping remediation reachable), and
6565
since ADR-0090 D12 every RBAC-table write is additionally checked by the
6666
**[delegated-admin gate](/docs/permissions/delegated-administration)**: tenant admins pass, delegates are confined to their
6767
`adminScope` (BU subtree + assignable-set allowlist, no self-escalation,
68-
strict containment for scope grants), and everyone else is denied. The
69-
`everyone`/`guest` audience-anchor bindings reject high-privilege sets at the
70-
data layer for every caller.
68+
strict containment for scope grants), and everyone else is denied. No caller
69+
writes either audience anchor's definition (`everyone`, `guest`), and a
70+
high-privilege set is refused at each point where one can reach `everyone`:
71+
accepting a package's `isDefault` suggestion answers `403 PERMISSION_DENIED`;
72+
every derivation of the anchor's definition (boot, accept, revoke, uninstall,
73+
package publish) leaves out a baseline, accepted or host-declared set that
74+
carries a forbidden bit and logs it; and the author-time rule
75+
`security-anchor-high-privilege` reports such an `isDefault` set as an error in
76+
`os validate`, `os build` and `os lint`, and the metadata door refuses to
77+
publish it (`422`). No door binds a set to `guest` until ADR-0138 D3 item 5's
78+
bind door is built ([Permission sets](/docs/permissions/permission-sets#package-suggestion--isdefault-adr-0090-d5)).
7179

7280
### The MCP execution surface (ADR-0096 / ADR-0101)
7381

0 commit comments

Comments
 (0)