Repository navigation
Commit 92fd343
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
69 | | - | |
70 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
71 | 79 | | |
72 | 80 | | |
73 | 81 | | |
| |||
0 commit comments