Skip to content

feat(plugin-security): register the packaged-permission-set lock at the metadata door - #12292

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-11843-metadata-door-permission-lock
Aug 25, 2026
Merged

feat(plugin-security): register the packaged-permission-set lock at the metadata door#12292
os-trump merged 3 commits into
mainfrom
claude/issue-11843-metadata-door-permission-lock

Conversation

@os-trump

@os-trumpos-trump commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11843

What

The pre-persistence authoring-gate seam in @objectstack/plugin-security — which carried an 'object' registration only — now also carries a 'permission' registration (registerPackagedPermissionSetLockGate, new file packaged-permission-set-lock-gate.ts, wired in security-plugin.ts beside the object posture gate) that consults the existingclassifyPackagedPermissionSet classifier from packaged-permission-set-lock.ts through the existingassertPermissionSetNotPackageDeclared write-door assertion, throwing the lock's existing error classes. Ruled by the maintainer on 2026-08-25 (「11843 同意」 — option B: keep NARROW, move the lock): one spelling of "package-declared", two doors, one refusal. No second refusal was authored.

The door, the layer, the refusal (the probe's legs are the acceptance oracle)

  • The metadata door (saveMetaItem) previously had no pre-write gate for permission; the feat(security): lock package-declared permission sets at the save door; clone to customize #11702 lock's only enforcement point was the sys_permission_set data door (probe report on the card, 04:43Z / 04:45Z).
  • Leg C inverted: with the OS_METADATA_WRITABLE=permission hatch open, a package-less save targeting a package-declared set is now refused with the lock's own error class — PackagedPermissionSetLockedError, NOT_OVERRIDABLE / 403 — and zero sys_metadata rows land. The pin asserts the row count, not only the throw — the defect was a write that landed.
  • Hatch closed: the identical save is refused at the same seam with the same class on the host-config topology (the gate runs ahead of the persistence doors); on scoped kernels the earlier ADR-0005 tier refusal still answers first — refused either way.
  • Leg F: no overlay row is minted, so nothing exists to win at read.
  • Leg B: the data door is untouched — packaged-permission-set-lock.test.ts is unmodified and green in the suite run.
  • Preservation (the ruling's NARROW): a hatch write to any non-packaged name lands exactly as documented — pinned with the gate registered: the row reads back package_id: null, organization_id: null. An ADR-0070 / ADR-0094 D5-R runtime-shadow set (definition living only in sys_metadata) stays editable: the classifier's sys_metadata sentinel exclusion carries over unchanged.
  • Fail-closed: when no provenance source can answer, the save refuses with PackagedPermissionSetProvenanceUnknownError (same code/status), exactly as the data door does.
  • The package-author channel is exempt by the seam's own contract (#4463 运行时发布门在 host-config 拓扑(environmentId 未绑定)上整体不跑——#5086 只把 code-only 拒绝移出了同一条短路 #6710 / owd-save-gate: the ADR-0090 D11 authoring gate never runs on any host-config deployment (keyed on environmentId, not authoringChannel) #7674), so package publishes never meet the gate. Draft saves are gated like publish saves (both minting paths).

The 39 pins

sys-metadata-repository.package-writability.test.ts39/39 green on the final head (e7dab8594), file untouched. They cover non-packaged names and remain authoritative per the ruling.

The new pin was red on the accepting behaviour (dissolution verification)

From the committed fix, the accepting behaviour was restored on disk (mutation verified by anchored grep: injected marker count 1, removed call-site count 0), and the pin went 4 failed | 3 passed:

AssertionError: expected null to be an instance of PackagedPermissionSetLockedError — hatch OPEN: the write accepted, i.e. the measured defect
AssertionError: expected Error: [NOT_OVERRIDABLE] 'permission' is … to be an instance of PackagedPermissionSetLockedError — hatch CLOSED: the refusal came from the tier gate, not the lock; the class assertion is the layer fingerprint doing its job

Restored via git checkout HEAD -- on the gate file; disk == index == HEAD verified by blob hash (0dadd6a9… on all three); re-run 7/7 green. No rebuild leg applies to the mutation: the pin resolves both the mutated module (relative import) and the protocol (vitest source alias added in this PR, whose comment states why) from source, so no dist/ sits in the loop.

Verification (final head e7dab8594; every heavy run through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe)

  • pnpm --filter @objectstack/plugin-security test — 83 files / 1539 passed, os-verify-lock: VERDICT command-exit 0
  • pnpm --filter @objectstack/plugin-security typecheck — clean, same VERDICT-0 chain
  • 39-pin file — 39 passed, VERDICT command-exit 0
  • Gate list derived mechanically (node scripts/pm/dispatch-gates.mjs, re-derived on the merged tree — list unchanged): changeset-gate-self-tests · objectui-changeset · override-consistency · published-files · slot-lookup · test-source-alias · type-source-resolution · adr-0087-registration · changeset-fixed · changeset-no-major · ci-filter-parity · empty-changeset · osv-exemptions · plugin-teardown-shape · cross-package-test-inputs · release-rehearsal-clone --self-test · docs-audit pair — all exit 0
  • Convention-triggered: engine-double-contract (the RETAINED ledger records the new pinned double — the gate's own printed remedy, --write and commit) · where-matcher · query-options-erasure · type-check-coverage · type-check-debt (--re-measure OK — none above its recorded number) · i18n (after building the CLI it requires) · i18n-stale-fill · nul-bytes — all exit 0
  • Declared narrowing: the repo-wide farm (pnpm lint and the rest of the 167 discovered families) is CI's run; the local scope is the derived list above plus the affected packages.

Scope notes


Generated by Claude Code

…he metadata door
The pre-persistence authoring-gate seam carried an 'object' registration
only, so a metadata-door save targeting a package-declared permission set
reached persistence whenever the ADR-0005 tier gate was open for the type
(the documented OS_METADATA_WRITABLE operator hatch) — and the resulting
overlay won at read. Per the maintainer ruling of 2026-08-25 (option B:
keep NARROW, move the lock), the seam now carries a 'permission'
registration that consults the same classifyPackagedPermissionSet
classifier and throws the same PackagedPermissionSetLockedError the
sys_permission_set data door already uses — one spelling of
"package-declared", two doors, one refusal. Hatch writes to non-packaged
names keep the documented behavior, and runtime-shadow (ADR-0070 /
ADR-0094 D5-R) sets stay editable.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
…act ledger
The gate's own remedy for new pinned coverage — the fake in
packaged-permission-set-lock-gate.test.ts routes both write verbs through
the shared dispatch predicates and the RETAINED ledger now holds the file.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts, packages/plugins/plugin-security/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts, packages/plugins/plugin-security/vitest.config.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 177: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8649b398b8cfb748b60222f4964729a35bc9e245packageMentionDocs.

Which tree this was computed on

This run read content/docs from ab889174800e53b618d42deac7003f21924a312c — the merge of head e7dab85944c8554fe176506bd91382e3ac937be8 into base 8649b398b8cfb748b60222f4964729a35bc9e245, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ab889174800e53b618d42deac7003f21924a312c && git checkout ab889174800e53b618d42deac7003f21924a312c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8649b398b8cfb748b60222f4964729a35bc9e245 e7dab85944c8554fe176506bd91382e3ac937be8 && git checkout -B drift-repro 8649b398b8cfb748b60222f4964729a35bc9e245 && git merge --no-ff e7dab85944c8554fe176506bd91382e3ac937be8
node scripts/docs-audit/affected-docs.mjs --json 8649b398b8cfb748b60222f4964729a35bc9e245

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract-review verdict: PASS (#11843)

Reviewed by the contract-review chain at CONTRACT_REVIEW_TIER. Tier fuse, machine reading this sub-round: get_session.external_metadata.last_served_model = claude-fable-5.

Reviewed: the gate file, plugin wiring and vitest-alias patches in full, and the ruling record on #11843.

Conformance to ruling B (maintainer 2026-08-25, verbatim 「11843 同意」 — keep NARROW, move the lock), checked against the ruling's own acceptance oracle:

  • One spelling, two doors, one refusal — verified in the gate file: registerPackagedPermissionSetLockGate calls the data door's own assertPermissionSetNotPackageDeclared, consulting the same classifyPackagedPermissionSet and throwing the same error classes (NOT_OVERRIDABLE/403, and the fail-closed provenance-unknown refusal). No second refusal was authored; code, status and clone prescription cannot drift because both doors run the same lines.
  • Leg C inverted with the layer fingerprint: the dissolution run's hatch-CLOSED failure — the refusal coming from the tier gate instead of the lock's class — is precisely the false-pass mode the probe warned about, and the class assertion caught it. Row-count asserted, not just the throw.
  • The ruling's hard condition holds: the 39-pin sys-metadata-repository.package-writability.test.ts is untouched and 39/39 green — non-packaged hatch writes keep the documented behaviour on both topologies, and the D5-R runtime-shadow exclusion carries over. The hatch's documented capability is retained, exactly as ruled.
  • Channel split by the seam's own contract: draft and publish saves gated, package-author channel exempt (#4463 运行时发布门在 host-config 拓扑(environmentId 未绑定)上整体不跑——#5086 只把 code-only 拒绝移出了同一条短路 #6710/owd-save-gate: the ADR-0090 D11 authoring gate never runs on any host-config deployment (keyed on environmentId, not authoringChannel) #7674). Data door untouched (leg B file unmodified).
  • The 'update' classification choice (refusal teaches the clone path, not rename) is reasoned in place; the dev-only vitest source alias exists for the right reason (a stale protocol dist/ would test a seam predating the registration) and adds no runtime edge.
  • Disclosure discipline held: shape and refusal stated, no recipe. Changeset minor states the one behaviour consequence plainly.

CI note: the Test Core (1/6) red drawn earlier was diagnosed on the card as runner port contention, not this diff's account — auto-merge will hold until the re-run is green; if it stays red on a clean re-run, that becomes real and comes back to this thread.

Landing completed by the chain in the same stroke per the maintainer's standing instruction (2026-08-25, verbatim: 「审核通过你应该直接负责合并吧,还要等原始的项目经理吗」): clearing needs:contract-review on this PR and #11843, flipping ready, arming the queue.


Generated by Claude Code

@os-trump
os-trump marked this pull request as ready for review August 25, 2026 17:37
@os-trump
os-trump added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 5619aacAug 25, 2026
71 of 73 checks passed
@os-trump
os-trump deleted the claude/issue-11843-metadata-door-permission-lock branch August 25, 2026 18:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-trump@claude