Repository navigation
Commit 5b68524
fix(metadata-protocol): an environment kernel that hydrates its own store writes a published body through to its registry, so the security catalog serves it at once (#23345)
Fixes #23298
Fixes #23233
Clause-②: no — the registry comes to agree with the stored row, its body
and its package binding; no schema, export or accept set moves.
## What this changes
On a host kernel, and on an environment kernel that hydrates its own
`sys_metadata` into its own engine registry, a permission set published
through the metadata door is in the security catalog at once, with its
new body and under the package it is bound to:
- a **narrowed** set resolves to its new body on its next resolution,
with no rebuild (before, on an environment kernel: the boot-time, wider
body until the kernel was rebuilt);
- a **new** set published after boot is in the registry and the catalog
at once, and an `isDefault` set records its `everyone` suggestion with
no list read and no rebuild (before, on an environment kernel: in no
reader at all; on a host kernel: unowned until a list read).
#23298's change is `284777c4f1` (with its changeset, pin and wording
commits `081af82c65`, `f8f9806db9`, `13cbde5d19`); #23233's stamp is its
own commit, `1b8c66d437`; `6c48c8e116` and `dd1ff6ce1b` update three
existing pins for the stamp (below).
## #23298 — the body: the write-through asks whose registry it is
`metadata-protocol`'s `applyRegistryWriteThrough` returned for every
type except `object` whenever the protocol had an `environmentId`. Its
stated reason — a project-scoped row must not be registered into a
registry that unscoped (control-plane) callers share — holds only for a
shared registry. A kernel whose boot hydrates its own store
(`loadMetaFromDb`) put every stored body into its registry at boot and
then never updated it.
- **New predicate `registryTakesStoredRows()`**: yes on an unscoped
kernel (as before); yes on an environment kernel once this protocol has
hydrated its own store into its registry; no on any other environment
kernel.
- **The ownership signal is the composition's own hydration call.**
`ObjectQLPlugin.start()` calls `protocol.loadMetaFromDb()` on an
environment kernel only when the composition declares
`hydrateMetadataFromDb`, whose stated precondition is that the kernel's
registry is per-instance isolated and its `sys_metadata` lives on its
own driver. `loadMetaFromDb` records that on the instance
(`storeHydratedIntoRegistry`). Both mounts (the built-in assembly and
`createMetadataProtocolPlugin`) reach it, because the plugin resolves
`protocol` lazily. ⛔ `environmentId` alone is not read as the answer. No
runtime or composition line changes.
- **The write-through** asks the predicate instead of `environmentId`.
- **The delete's heal** (`restoreArtifactRegistryView`, tier 2:
re-register a MetadataService baseline) asks the same predicate. Without
it, a save that now registers its body, followed by a reset of an item
whose baseline the MetadataService holds, left the deleted body in the
registry (ablation C).
- **The `object` branch is unchanged.** It never carried the
kernel-scope gate (no packaged-object overlay exists at any scope;
`assertObjectRegistered` fails closed); that reason is untouched.
- `getMetaItems`' list-read hydration keeps its `environmentId ===
undefined` gate.
### Stage 8's note, now true on a hydrating environment kernel
`packages/plugins/plugin-security/src/security-plugin.ts` (C3 stage 8,
`66949b7729`):
> [ADR-0131 D3, C3 stage 8] No `permission` mutation projector: the
metadata door's own registry write-through puts a saved set in the
security catalog, and no row mirrors it.
That held on an unscoped kernel only. It now holds on every kernel whose
registry takes its stored rows, and with the set's owner. On an
environment kernel whose store is never hydrated the write-through still
writes nothing (the registry may be shared); the retired projection echo
is not restored.
## #23233 — the owner: the shared hydrator stamps the row's package
binding
`hydrateOverlayIntoRegistry` is the one hydrator the boot
(`loadMetaFromDb`) and the write-through share. It registered a runtime
package's body without the row's package binding, so the security
catalog served the set unowned — on a host kernel until a list read
stamped it, on a hydrating environment kernel indefinitely — and since
PR #23311 the suggestion collector reads the owner from the catalog
entry alone, so an `isDefault` set recorded no `everyone` suggestion.
- The registered body now carries the row's binding as `_packageId`
(`withRowBinding`), the way `getMetaItems` stamps it from the row's
`package_id` before it hydrates.
- ⛔ No owner is derived: the binding is only read. A row bound to no
package (empty or the `sys_metadata` sentinel) stays unowned; a body
that already names `_packageId` keeps it; an artifact's envelope still
wins (`mergeArtifactProtection`); a view container's expansions are
registered from the unstamped row, as before.
## Measured (merged head, real `ObjectQL` engine and `SchemaRegistry`,
real protocol, real `saveMetaItem` publish, real
`createSecurityCatalogReader`, real `syncAudienceBindingSuggestions`)
| case | BEFORE | AFTER |
|---|---|---|
| narrowed set, hydrating environment kernel: catalog resolution after
the publish | `protocol.ts` at `404fc477e5`: source `registry`,
`allowDelete: true`, no `isDefault` (the boot-time body) | source
`registry`, `allowDelete: false`, `isDefault: true`, equal to what a
rebuild reads |
| view re-published, hydrating environment kernel: `registry.getItem` |
old label | new label, equal to what a rebuild reads |
| new `isDefault` set, hydrating environment kernel, body without
`packageId` | `protocol.ts` at `404fc477e5`: registry `false`, catalog
`null`, suggestions `[]`; with #23298 only, on the merged tree (PR
#23311 landed; that run's body still declared its `packageId`): registry
held, catalog entry unowned, suggestions `[]` | registry held, catalog
source `registry` owned by the runtime package, one `pending` `everyone`
suggestion |
| new `isDefault` set, host kernel, no list read | owner unread (stamp
dropped, ablation D): catalog unowned, suggestions `[]` | one `pending`
`everyone` suggestion; catalog and protocol reads both name the runtime
package |
## Pins
-
`packages/objectql/src/protocol-environment-kernel-registry-write-through.test.ts`
(dist-resolved protocol): narrowed set and view on a hydrating
environment kernel read the new body with no rebuild, equal to a
rebuild's; a reset of an item with a MetadataService baseline puts the
baseline back; a shared registry never receives a project-scoped
permission set or view (control: the control-plane protocol's own
publish reaches it).
-
`packages/plugins/plugin-security/src/permission-set-env-kernel-registry-write-through.test.ts`
(source-aliased protocol), every set published with no body `packageId`:
- #23298: on a hydrating environment kernel, a new set is in the
registry and the catalog under its package and its `isDefault` records
one pending `everyone` suggestion;
- #23233, host kernel, right after the publish: exactly one `everyone`
suggestion; the catalog read and the protocol read name the same owner;
- controls: a code package's default set is unchanged (its artifact, its
owner, its suggestion) on both kernels; a set saved into no package
stays unowned with no suggestion.
## Existing pins the owner stamp changed (the CI reds on `1b8c66d437`)
- `Test Core (6/6)`, `metadata-protocol` —
`protocol.register-item-call-sites.test.ts`, "the calls in the source
are exactly the recorded ones" and "each registration that skips a name
another package ships asks anotherPackageShips": first error "a
registration with no recorded disposition". Cause: the ledger keys each
`registerItem(` call by its source text, and the hydrator's call now
passes `withRowBinding(data, options.packageId)`. Fix: the recorded call
text is updated; its disposition (a view row bound to a package skips a
name another package ships) is unchanged, and the `path` text names the
write-through's new kernels.
- `Test Core (5/6)`, `objectql` —
`protocol-boot-hydration-scoped.test.ts`, "grafts NO artifact envelope
when artifacts have not loaded yet": first error "expected 'com.acme.b'
to be undefined". Cause: the pin asserted `_packageId` absent because
only an artifact graft could set it. Fix: it asserts the row's own
binding; `_lock` absent and `_provenance: 'org'` still prove no graft.
- `objectql` — `protocol-derived-provenance-doors.test.ts`, "with NO
artifact present the restatement stands and nothing is invented": first
error "expected 'app.sdbh' to be undefined". Same cause; it asserts the
row's binding and adds `_packageVersion` absent.
Both `objectql` pins encoded the missing-owner shape, and ablation D
turns both red. No test was skipped, deleted or loosened.
## Ablations (`scripts/ablation-replace.mjs`; every restore proven: blob
equal to HEAD, `git diff HEAD` empty, whole tree clean)
`objectql` reads `@objectstack/metadata-protocol` from `dist/`, so each
leg rebuilt it and `scripts/ablation-dist-preflight.mjs` proved the
marker in or out of the artifact; `plugin-security` aliases it to
source.
- **A — the bare `environmentId` gate restored** (`1b8c66d437`):
narrowed set, view and reset pins red; the environment-kernel new-set
pin red; the shared-registry pins, the host-kernel #23233 pins and the
controls green.
- **B — the shared-registry guard removed** (`1b8c66d437`): only the
shared-registry pin red (the project-scoped set reached the shared
registry).
- **C — the heal's re-registration back to control-plane-only**
(`1b8c66d437`): only the reset pin red (the deleted body stayed
registered).
- **D — the stamp dropped** (`1b8c66d437`, again at `dd1ff6ce1b`): the
environment-kernel new-set pin, both host-kernel #23233 pins, and (at
`dd1ff6ce1b`) the two updated `objectql` pins red; the code-package and
unowned controls and every #23298 `objectql` pin green.
- **Restore** (`dd1ff6ce1b`): the stamp call present in `dist/`, the A,
B and C markers absent, tree clean; `objectql` 35 of 35,
`plugin-security` 6 of 6.
## Tests (final head `dd1ff6ce1b`; `main` merged at `abf96ffac8`)
- `metadata-protocol` full: 235 files passed, 3 skipped; 28353 tests
passed.
- `objectql` full in three chunks: 140 files / 3045 tests at
`1b8c66d437` (no source or file in that chunk changed after), 139 / 2186
and 136 / 2840 at `dd1ff6ce1b`.
- `plugin-security` full: 190 files, 3872 passed, 45 skipped.
- `runtime` env_local and registry-reading suites (13 files, 340 tests)
and `service-automation` `flow-name-shadowing` (14 tests).
- `typecheck`: `metadata-protocol`, `objectql` (test layer held at its
ledger), `plugin-security` (test layer 0 errors).
- `dispatch-gates --ran` at `dd1ff6ce1b`: 71 derived, 71 run, every
recorded exit 0 (`check:dual-build-cjs-loads` and `check:i18n` first
refused on an unbuilt tree, exit 3, and passed after a full build).
- Lint at `dd1ff6ce1b`, a measured narrowing: the six changed source
files are in the `eslint.config.mjs` population; `--format json` reports
6 files, 0 errors, 0 warnings; the config enables no type-aware linting
(no `parserOptions.project`), so this diff cannot move an untouched
file's verdict. The repo-wide `pnpm lint` is CI's.
- The `cli` integration layer and the dogfood battery are declared to
CI.
## Docs drift check
`content/docs/kernel/cluster.mdx` (lines 340–360 and 420–460, which name
`applyRegistryWriteThrough` at 352, 433 and 440) and
`content/docs/concepts/metadata-lifecycle.mdx` (lines 40–64, which name
`ObjectStackProtocolImplementation` at 47, the write path at 70–100, and
the peer note at 205–225) were read on the merged head. Neither states
the old rule: neither says the write-through is skipped on environment
kernels, nor that a saved permission set reaches the registry only at
boot or on a list read. No docs edit.
## Acceptance notes
- **Every type, not only `permission`.** On a hydrating environment
kernel every non-`object` type now writes through, as on an unscoped
kernel and as the boot already registers it; measured for a permission
set and a view. A view container's republish reconciles its expansions
there by the same code path (not separately measured on an environment
kernel).
- **Every hydrated row now carries its binding** as `_packageId` on the
boot and the write-through, as a list read already did on a host kernel;
registry readers that filter by package see the row in its package from
the first registration.
- **Relation to #23334 (#23308).** Its metadata-service echo covers
environment kernels that do not hydrate and registers only names the
engine registry does not serve; the two touch no common file, and the
later lander re-runs the other's pins.
- The new `plugin-security` test file is outside `metadata-protocol`;
the suggestion collector it drives lives there.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 707b3cd commit 5b68524
7 files changed
Lines changed: 814 additions & 36 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
- plugins/plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
| 99 | + | |
99 | 100 | | |
100 | | - | |
| 101 | + | |
| 102 | + | |
101 | 103 | | |
102 | 104 | | |
103 | 105 | | |
| |||
0 commit comments