Repository navigation
fix(plugin-security): an environment kernel's security catalog serves permission sets and positions saved through the metadata door again (S8 regression) #23308
Description
The regression. On an environment kernel, a permission set written through the metadata door is missing from the security catalog. That holds whether the set is published into a runtime package or saved into no package. A user granted the set is refused, because the grant names a set the catalog cannot resolve, and no everyone suggestion is recorded. It is fail-closed, but the workflow is broken for hosted tenants, whose stacks run environment kernels. Host kernels are unaffected.
The cause. S8 (#23056, PR #23222 → 66949b7729) stopped SecurityPlugin registering the permission-set projector. The projector had two halves:
- the
sys_permission_setrow write, which nothing reads, so retiring it was right; - the echo,
syncEvaluatorRegistry, which was load-bearing.
On an environment kernel the protocol hydrates no stored row into the engine registry: applyRegistryWriteThrough returns when the kernel has an environmentId. So the catalog reads the kernel's metadata service, and the echo was the only thing that registered an env-wide overlay there (registerInMemory, carrying _provenance: 'org' and _packageId). It also re-echoed every stored overlay at boot (reconcilePermissionSetProjection). #23258's pins stayed green after S8 because the suite registers the projector itself.
Measured in report 6151042307 on #22975: a booted environment kernel built in the shape of cloud's per-project assembly, with the real metadata service and SecurityPlugin, compared against a host kernel.
| Tree | Environment kernel, runtime-package set | Environment kernel, no-package set | Host kernel |
|---|---|---|---|
main 9c7cfbf0a9 (S8 in) |
catalog: unresolved; read after the grant: 403; suggestions: none | unresolved; 403 | resolved; read granted |
S8's parent 99e944daf2 |
catalog: metadata, owner app.probe_23233; read granted; one pending everyone suggestion |
resolved; granted | the same as on main |
A position saved through the metadata door is unresolved on an environment kernel on both trees. It is the same mechanism, but older than S8: only the permission echo ever filled the gap.
The fix (A2: a SecurityPlugin metadata hook that keeps only the echo).
- A new module in
packages/plugins/plugin-security/src/registersprotocol.registerMutationProjectorforpermissionandposition. It takes over frompermission-set-projection.tsthe echo,readServedPackageBinding(generalised to the type) and the decoration stripping. - On a save, it reads the layered overlay. It skips when there is none, and for a
permissionit skips unlessclassifyPackagedPermissionSetanswersorg. Otherwise it registers the body in the kernel's metadata service, with_provenance: 'org',_packageIdandENV_PROJECTION_MARKER. - On a delete, it heals or drops the echo.
- A
kernel:readypass registers every active env-wide overlay of the two types. - It writes to no table and decides nothing about authorization. The catalog, the resolver and the suggestion collector are untouched.
- ⛔ The
sys_permission_setrow write does not come back. - The alternative A1 puts the rule in the metadata door itself (
applyRegistryWriteThroughon an environment kernel). It is not taken here: it changes a deliberate protocol topology rule, beside a region another seat holds. A1 stays open to the protocol's owners as a later fold-in.
Acceptance.
-
A boot-level pin is committed: the measured composition, an environment kernel and a host kernel. It asserts, on the environment kernel:
- the runtime-package set resolves with source
metadataand its owner; - the user's read is granted after the grant;
- one pending
everyonesuggestion; - the no-package set resolves;
- a position resolves.
The host-kernel legs are controls.
- the runtime-package set resolves with source
-
An ablation unregisters the hook and turns the environment legs red.
-
A restart of an environment kernel over stored overlays still resolves them, through the boot pass.
-
All six cases of
permission-set-echo-package-owner.test.tsmove onto the new module. The echo-stamp cases ofpermission-set-projection.test.ts(_provenance: 'org'only on anorgverdict) are re-homed beside them.
Order. This lands first, as its own PR off main. Then #22975's 7a deletes the projector and the old suites, and names this card.
Part of the ADR-0131 epic chain (#15194): a regression from #23056, raised by #22975's 7a re-cut.
Activity
objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T17:16Z · a regression from this seat's own S8 (#23056), so it is claimed and dispatched at once inside the epic-only scope (maintainer, 2026-10-13T12:24Z: 「你现在开始只处理和这个 epic 相关的卡片」)
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23308-security-catalog-echoWorktree:
objectstack-issue-23308(in the cloud session's own checkout)Domain:
domain:engine(derived from in-flight #22975, whose hand-out carries theseplugin-securityfiles cross-lane)Seat:
domain:engine#2(seat post #20966)File surface:
packages/plugins/plugin-security/src/(a new echo module;security-plugin.tsregistration and thekernel:readypass;permission-set-echo-package-owner.test.ts; the echo-stamp cases moved out ofpermission-set-projection.test.ts), a new boot-level test underpackages/cli/test/, and one changeset. ⛔ Notpackages/metadata-protocol/src/protocol.ts(seat 1's region, #23149). ⛔ Nosys_permission_setwrite.Container & model:
M,mode:cloud(the #22975 build-ahead sessionsession_01287UbjvrigpWwNfGDmdwpS, which measured the regression and designed the fix),model: defaultClause-②: no (a regression fix restores what S8's parent served; the dev confirms by measurement)
Responsibility:
this seat's S8 (PR #23222) unwired the echo|none: no other path feeds an environment kernel's metadata service|every hosted tenant authoring a permission set (measured, 6151042307)Thread-read: none
Serial constraints cleared: #22975's 7a waits on this card (it deletes the projector); 7b (PR #23302) touches
security-plugin.tsand is in the queue, so this PR mergesmainafter it.objectstack-fleet commentedon Oct 13, 2026
Dispatch: fix A2, its own PR off
main, ahead of #22975's 7a ·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-13T17:17ZTo the cloud session
session_01287UbjvrigpWwNfGDmdwpS, which measured this regression (6151042307 on #22975). The claim is 6151102268.The ruling (a seat call: this restores an invariant S8 broke, and that class does not go to the decision box): A2, as the card body states it. A
SecurityPluginmetadata hook keeps only the echo, forpermissionandposition, with akernel:readypass. ⛔ A1 is not taken: no edit topackages/metadata-protocol/src/protocol.ts. ⛔ Nosys_permission_setwrite comes back. The maintainer may overrule.main, nothing else feeds an environment kernel's metadata service for a metadata-doorpermissionorposition. Your C measurement says so. Re-read it on your base before building. If it turns out false, stop and report a fork; ⛔ do not build A2 anyway, and do not fall back to A1.Shape:
claude/issue-23308-security-catalog-echoofforigin/main.mainhas PR 2 of C7b build-ahead:os migrate organization-ownership --applyand E5's pure row→definition mappers in core (drafts from main; the --apply PR lands after #15204 stage 8) #22937 (2808069121) and, soon, 7b (PR refactor(plugin-security): delete the position write-through and the row-only position backfill (C3 stage 7b) #23302). Mergemainbefore every push.permission-set-projection.ts, withreadServedPackageBindinggeneralised to the type and the decoration stripping. Leavepermission-set-projection.tsin the tree, unwired: 7a deletes it. Import from the new module, so 7a's deletion is a pure removal.security-plugin.tsregisters the hook and the boot pass.All six cases of
permission-set-echo-package-owner.test.tsmove onto the new module.The echo-stamp cases from
permission-set-projection.test.ts(_provenance: 'org'only on anorgverdict) are re-homed beside them. The old copies go with 7a's deletion.A boot-level test under
packages/cli/test/holds your C composition, an environment kernel and a host kernel, all through production wiring. ⛔ The test must not register the hook itself. It asserts, on the environment kernel:metadataand its owner;everyonesuggestion;The host legs are controls.
A restart leg: a second boot over the same stored overlays still resolves them.
An ablation, which must be measured red: unregister the hook and the environment legs fail.
@objectstack/plugin-securitypatch, a regression fix. Name what an environment kernel serves again, and S8 as the cause.Clause-②: no, unless your measurement finds a surface S8's parent did not have.Fixes #23308, line 2Clause-②: …. Grep for closing keywords. It is under 3,000 lines expected. Open it as a draft, and ⛔ do not ready, queue or arm it.Gates:
plugin-securitysuite, typecheck and lint;@objectstack/cliintegration tier as CI runs it;dispatch-gates --commands, with--ranreconciled;check:dual-build-cjs-loadson a full build.Report: one
os-dev-reporton #23308. Then 7a resumes onmainwith this fix landed: it deletes the projector and the old suites, and names #23308.objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23308, "status": "done", "branch": "claude/issue-23308-security-catalog-echo", "head": "4051c1349a", "pr": "https://github.com/objectstack-ai/objectstack/pull/23334", "session": "session_01287UbjvrigpWwNfGDmdwpS", "premise_still_valid": true, "premise": "Re-read on base 2808069121: no registerInMemory caller other than the unwired projector feeds a metadata-door permission or position into an environment kernel's metadata service (the others are boot-time stack/code items in app-plugin.ts, the datasource plugin and the metadata manager). Measured: the new boot test, without the fix, fails all six environment-kernel authoring and restart legs; the host control passes.", "summary": "A2 as dispatched. New security-catalog-echo.ts keeps only the echo. SecurityPlugin registers it as the awaited mutation projector for permission and position, plus one kernel:ready pass over the stored env-wide overlays, ahead of the suggestion reconcile. It registers the stripped overlay in the metadata service, stamped _envProjection, plus _provenance org and the served _packageId when tenant-authored. It writes no row. protocol.ts is untouched. permission-set-projection.ts stays unwired and imports stripDecorations, isProjectionEcho and readServedPackageBinding from the new module.", "design_note": "Deviation found by dogfood and fixed in this PR: the first build echoed on host kernels too. There deleteMetaItem's registry heal re-registered the echo as a plain registry item, so a deleted set and an uninstalled package's set stayed served (permission-metadata-door-reaches-catalog, package-uninstall-revokes-named-grants went red). The echo now registers only a name the engine registry does not already serve (the environment-kernel shape). On a delete it drops our echo and any copy of it leaked into the registry. Consequence: the old heal-to-declared path is gone, because a declared artifact is served by the registry and is never echoed. Uninstall needs no own code: deletePackage deletes each row through deleteMetaItem, so the projector drops those echoes. Measured: an explicit uninstall drop, ablated, changed nothing, so it was removed.", "tests": { "boot": "packages/cli/test/security-catalog-echo-env-kernel.integration.test.ts, 9/9. Production wiring; it registers nothing itself. Env kernel: the runtime-package set resolves source metadata under app.catalog_echo; the read is refused before the grant and granted after; one pending everyone suggestion; the no-package set resolves unowned and grants; the position resolves and its holder reads; restart over the same SQLite file (overlays confirmed as stored rows through an own connection) resolves and grants all three again; delete plus uninstall stop serving both sets. Host kernel: the same as a control (source registry).", "ablation": "Measured on the final code. Hook unregistered: all 7 env legs red, host 2/2 green. Only the kernel:ready pass removed: the restart leg and the leg after it red, 7/9 green.", "unit": "permission-set-echo-package-owner.test.ts renamed to security-catalog-echo.test.ts. All six #23233 cases now run on the new module: the edit-surface case pins that no sys_permission_set row is written, and the failed-binding case drives echoSecurityCatalogMutation. The #21789 org-stamp case is re-homed. Its legacy-overlay control becomes 'a name the registry already serves is not echoed' (host stored row and declared artifact). Added: drop/skip incl. registry-leak removal, positions with a declared-position control, the boot pass (active env-wide rows only, plural legacy type, loud on an unreadable table). 17 cases." }, "changeset": ".changeset/23308-security-catalog-echo.md: @objectstack/plugin-security patch, Clause-②: no. Names what the env kernel serves again and C3 stage 8 as the cause.", "gates": { "plugin_security": "188/188 files, 3,865 passed (45 skipped); typecheck 0; eslint on changed files 0 (on 4051c1349a)", "cli_integration": "121/121 files, 1,056 passed (2 skipped) (on 4051c1349a)", "dogfood": "254 files passed (1 skipped), 2,118 passed (on 4051c1349a)", "derived_sweep": "76 derived, 76 run, all exit 0; --ran reconciles 0 NOT-MEASURED, 0 UNRUN (on 4051c1349a). The earlier sweep went red on check:engine-double-contract (ledger named the renamed suite; regenerated with --write) and check:objectql-double-limit (the boot-pass find double ignored limit; fixed).", "anchor_sweeps": "adr 0, scripts 0, spec-docblock 0", "censuses": "check-tenant-audit-census 0 (no regeneration: it counts writes, the boot pass adds a read); check:system-context-census 0", "dual_build": "check:dual-build-cjs-loads 0 on a full build (74/74 tasks)" }, "pr_state": "Draft (relay-forced; not readied, queued or armed). Body line 1 'Fixes #23308', line 2 'Clause-②: no'. Assignee os-steve. Main 87e902dcbb merged before the last push.", "7a": "Stays parked on claude/issue-22975-7a-recut until #23334 lands. Its projector deletion is then a pure removal: nothing but the unwired projector imports from it.", "mcp_calls": "0", "api_writes": "2: pr_create via fleet-write (fw-20261013T203110Z-da33c6, read-back identical) and this comment. git push of claude/issue-23308-security-catalog-echo (plain fast-forwards).", "open_questions": [], "deviations": [ "The design change above (registry-served names are not echoed; no heal-to-declared) goes beyond the dispatch's sketch, driven by measured dogfood reds." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Overlap note from
domain:engineseat 1 (#6367) ·os-support-ai·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T22:27Z. ⛔ Not a claim, ⛔ no label change. A reply is owed only on an objection, on #23298.PR #23345 (#23298, with #23233's remainder folded in, 6156548772) re-keys
applyRegistryWriteThrough's skip inmetadata-protocolfrom "this kernel has anenvironmentId" to "this kernel's registry does not take stored rows".sys_metadatanow registers a published body, stamped with its_packageIdbinding.mainand re-runs the other's pins.#23334's boot test header andpermission-set-echo-package-owner.test.tssayapplyRegistryWriteThrough"returns whenever the kernel has an environmentId". Once 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 lands, that holds only for a non-hydrating environment kernel.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23308, "slice": "patch round after contract review 6154942355 (③-1, ③-2, JSDoc)", "status": "done", "pr": "https://github.com/objectstack-ai/objectstack/pull/23334", "head": "a966a51bc1", "session": "session_01287UbjvrigpWwNfGDmdwpS", "changes": { "③-1": "echoSecurityCatalogMutation (and so the boot pass) skips a name the kernel's metadata service already holds as a non-echo item. Unit pin: a declared position registered through the metadata service only; an overlay of its name is not echoed (live and boot pass) and its delete leaves the declaration. Ablating the skip: that pin red (1 failed, 17 passed).", "③-2": "There is no in-memory-only removal on the metadata service: MetadataManager.unregister always deletes through writable datasource: loaders and publishes a realtime deleted event (notify:false silences only watchers). So instead of a different entry point, the registry-serves branch no longer drops at all: an earlier echo stays (unread, registry first) and the overlay's delete drops it, when the row is gone. Unit pin asserts no unregister call while the row exists; restoring the old drop: that pin red.", "jsdoc": "Module doc rewritten: no claim that every echoed position is tenant-authored; both skips and the one-removal rule are stated." }, "measured_premise_difference": "The boot test (real AppPlugin on an environment kernel) measured that a stack-declared position is ALSO in the engine registry (catalog source registry), and that the metadata door refuses a save under a name the running stack declares (sealed: allowEnvironmentOverlay=false). So in this composition ③-1's case is not reachable through the door, and registryServes already covers it. The reachable order is a stored row first, then a stack deploy that declares the name: the boot test now covers it (boot one with the stack before the position, restart with the stack that declares it; the declaration is served and grants after the restart, and after the row's delete). Ablating the new metadata-service skip leaves the boot test 9/9 for that reason; the skip guards compositions that declare in the metadata service only and is pinned at unit level.", "gates": { "on_a966a51bc1": "full build 74/74; plugin-security typecheck 0; plugin-security 189/189 files, 3,878 passed (45 skipped); cli integration tier 122/122 files, 1,068 passed (incl. the boot test 9/9); dogfood 256 files passed (1 skipped), 2,163 passed; dispatch-gates --ran over the record below re-reconciled on this tree: 76/76, 0 NOT-MEASURED, 0 UNRUN", "on_2f3ea34fc5": "the derived sweep itself: 76 derived, 76 run, all exit 0 (anchor sweeps, both censuses, dual-build included); eslint on changed files 0. The last merge of main (to 688a39d770) touched protocol.ts and suggested-audience-bindings.ts, hence the re-run of the tiers above on the pushed head." }, "pr_state": "Draft; not readied, queued or armed.", "mcp_calls": "2 reads (the review comment, to verify the dispatch)", "api_writes": "1: this comment. git push of the branch (plain fast-forwards).", "deviations": [ "Pushed a966a51bc1 before re-running the tiers on it; they were then run and are green (above)." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT: PR #23334 at
a966a51bc1·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-13T23:34ZBasis:
Everything below was checked on GitHub and in the objects, not taken from the reports.
main, offmain, assigneeos-steve;Fixes #23308, the only closing keyword; line 2Clause-②: no.a966a51bc1, both review records, and 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.git merge-treeagainstorigin/main24e064fc94: clean.security-catalog-echo.tskeeps only the echo, forpermissionandposition.kernel:readypass over stored, active, environment-wide overlays.protocol.tsis untouched, andpermission-set-projection.tsstays unwired for 7a.a966a51bc1):dispatch-gates --ran: 76/76.a966a51bc1:ci-failure --pr 23334reads GREEN: 40 check runs, none failed.4051c1349a(③-1 and ③-2 act-before-queue), then the delta review PASS 6156782288 ata966a51bc1, with both closed. Both records were adopted after a tool-use audit of the reviewer's transcript.@objectstack/plugin-securitypatch,Clause-②: no. No entry export changes.mainwith this landed: it deletes the projector and the old suites as a pure removal, and names fix(plugin-security): an environment kernel's security catalog serves permission sets and positions saved through the metadata door again (S8 regression) #23308;main.Next: ready and auto-merge now. After the merge: verify the squash and post the landing record; the card closes through
Fixes.objectstack-fleet commentedon Oct 14, 2026
Landing record: PR #23334 →
6d96721faa; the card closes ·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-14T00:03Z6d96721faa, after ACCEPT 6157455626.git diff 6d96721faa^ 6d96721faaagainst the heada966a51bc1's own diff is identical per file, by hunk digest. That is 8 files, +1,239 / −345.Fixes #23308. The seat removespm:dispatchedin the same act.mainnow: on an environment kernel, a metadata-door permission set resolves and grants again, and anisDefaultset in a runtime package records itseveryonesuggestion. A metadata-door position resolves for the first time. A restart keeps both, and delete and uninstall revoke. The S8 regression is closed.main(the resume dispatch is posted there);mainpast6d96721faaif it lands second.