Skip to content

fix(plugin-security): an environment kernel's security catalog serves metadata-door permission sets and positions again - #23334

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-23308-security-catalog-echo
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-23308-security-catalog-echo

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23308
Clause-②: no

On an environment kernel, a permission set or position written through the metadata door is in the security catalog again: it resolves, it grants, and an isDefault: true set in a runtime package records its everyone suggestion. This is the regression C3 stage 8 (#23222) introduced, fixed as A2: a SecurityPlugin hook that keeps only the echo.

Why it broke

The security catalog (ADR-0131 D3) reads the engine registry first, then the kernel's metadata service.

  • On a host kernel, the metadata door hydrates every stored row into the engine registry, so nothing else is needed.
  • On an environment kernel, it does not: applyRegistryWriteThrough returns before the hydrator when the kernel has an environmentId. The catalog can find a stored set only in the metadata service.

Only one thing put stored sets there: the permission-set projector's in-memory echo (syncEvaluatorRegistry). Stage 8 unwired the projector, and the echo went with it. Since then, on an environment kernel, every metadata-door set:

  • resolves to nothing;
  • grants nothing (fail-closed);
  • records no suggestion.

permission-set-echo-package-owner.test.ts stayed green because it wired the projector itself. Positions never had an echo, so on an environment kernel a metadata-door position was unresolved before stage 8 too.

Premise, re-checked on this base. Nothing else on main feeds an environment kernel's metadata service for a metadata-door permission or position:

  • The other registerInMemory callers are boot-time stack and code items in app-plugin.ts, the datasource plugin and the metadata manager itself.
  • Measured: the new boot test below, run on main 2808069121 without the fix, fails all six environment-kernel legs. The host control passes.

What this changes

  • New security-catalog-echo.ts. It holds the echo alone: stripDecorations, isProjectionEcho and readServedPackageBinding (generalized to the type) moved out of the projector.
    • On a landed save, publish or delete of a permission or position, it reads the environment-wide overlay through getMetaItemLayered.
    • It registers the stripped body in the metadata service, stamped _envProjection. When the item is tenant-authored it also gets _provenance: 'org' (plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789) and the owning _packageId ([finding] The permission-set projection drops _packageId, so an isDefault set in a runtime package records no everyone suggestion — and two framework reads disagree on its owner #23233).
      • For a permission set, "tenant-authored" is the packaged-permission-set lock's verdict, as before.
      • A position is echoed only when no declaration of its name is served (next point), so an echoed position is always the stored overlay alone.
    • It echoes only a name that the engine registry does not serve and that the kernel's metadata service does not already hold as a non-echo item, such as a declaration an app plugin registered there. That is the environment-kernel shape, and a declaration is never shadowed by an overlay's echo.
      • On a host kernel the stored row is in the registry, so the catalog never reads an echo.
      • A first build echoed there too. Dogfood caught it: deleteMetaItem's registry heal re-registered the echo as a plain registry item, so a deleted set and an uninstalled package's set went on being served (permission-metadata-door-reaches-catalog, package-uninstall-revokes-named-grants).
      • A declared artifact is likewise never echoed.
    • When the overlay is deleted, it drops our echo once the stored row is gone, plus any copy of it that leaked into the registry. Nothing is dropped while the registry serves the name. The metadata service has no in-memory-only removal: its unregister also deletes through writable datasource: loaders and publishes a deleted event. A package uninstall deletes each row through deleteMetaItem, so the same projector covers that case.
    • It writes no row: no sys_permission_set write comes back. It decides nothing about authorization.
  • security-plugin.ts registers it as the protocol's awaited mutation projector for both types. On kernel:ready (runBootstrap) it runs one pass over every stored, active, environment-wide overlay of both types, before the suggestion reconcile reads the catalog.
  • permission-set-projection.ts stays in the tree, unwired. It imports the moved helpers from the new module, so 7a's deletion of it is a pure removal.
  • No edit to packages/metadata-protocol/src/protocol.ts.
  • No export of the package entry changes.

Tests

  • packages/cli/test/security-catalog-echo-env-kernel.integration.test.ts (new). This is the boot-level pin, and it wires nothing itself. It boots cloud's per-project composition: an environment-scoped ObjectQLPlugin plus createMetadataProtocolPlugin, the real metadata service, AuthPlugin and SecurityPlugin, over a SQLite file. It asserts on the environment kernel:

    • the runtime-package set resolves with source metadata under its owning package;
    • its read is refused before the grant and granted after;
    • one pending everyone suggestion is recorded;
    • the no-package set resolves unowned and grants;
    • a position resolves, and its holder reads through the set it names;
    • after a restart over the same file, with the overlays confirmed as stored rows through a connection of the test's own, all three resolve and grant again;
    • after a deleteMetaItem of the no-package set and a deletePackage of the runtime package, neither set is served or grants, while the position still resolves;
    • a row stored first, then a redeploy whose stack declares the same position name: after the restart the declaration is served, not the row, and deleting the row leaves the declaration served.

    The host kernel runs the same scenario as a control.

  • Ablation, measured on the final code:

    • With the hook not registered, all seven environment legs fail and both host legs pass. Without the fix, main fails the six authoring and restart legs the same way.
    • With the projector kept and only the kernel:ready pass removed, only the restart leg and the leg after it fail.
  • security-catalog-echo.test.ts (renamed from permission-set-echo-package-owner.test.ts):

Acceptance notes

Verification

On head a966a51bc1 (main 688a39d770 merged in):

  • Full build: 74/74 tasks. plugin-security typecheck: exit 0.
  • plugin-security unit suite: 189/189 files, 3,878 passed (45 skipped).
  • The full CLI integration tier (--project integration): 122/122 files, 1,068 tests passed, including the boot test, 9/9.
  • Dogfood: 256 files passed (1 skipped), 2,163 tests passed. This includes permission-metadata-door-reaches-catalog and package-uninstall-revokes-named-grants.
  • dispatch-gates --ran re-reconciled on this tree: 76/76, with 0 NOT-MEASURED and 0 UNRUN.

On 2f3ea34fc5, the patch round's head before the last merge of main:

  • The derived sweep itself (dispatch-gates --commands): 76 derived and 76 run, all exit 0. That includes the three symbol-anchor sweeps, both censuses and check:dual-build-cjs-loads. eslint on the changed files: exit 0.
    • An earlier sweep went red on check:engine-double-contract, because the ledger still named the renamed suite. It was regenerated with --write.
    • It also went red on check:objectql-double-limit, because the boot-pass find double ignored limit. The double now applies it.
  • The last merge of main touched protocol.ts and suggested-audience-bindings.ts, so the tiers above were run again on the pushed head.

Also:


Generated by Claude Code

…s into an environment kernel's security catalog

On an environment kernel the metadata door hydrates no stored row into the
engine registry, so the security catalog can find a metadata-door permission
set or position only in the kernel's metadata service. The permission-set
projector's echo was the only thing that put a set there, and C3 stage 8
unwired the projector: since then such a set resolved to nothing, granted
nothing and recorded no everyone suggestion. Positions never had an echo.

A new module, security-catalog-echo.ts, keeps only the echo: SecurityPlugin
registers it as the awaited mutation projector for permission and position,
and runs one pass over the stored env-wide overlays at kernel:ready. It
writes no row. The projection module (unwired) now imports the shared
helpers from it.

The echo-owner pins move onto the new module with the #21789 provenance
cases, and a boot-level CLI test drives an environment kernel and a host
kernel through the production wiring, across a restart.

Claude-Session: https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS
Co-authored-by: Claude <noreply@anthropic.com>
…e's rename; the boot-pass find double holds its bound

Claude-Session: https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS
Co-authored-by: Claude <noreply@anthropic.com>
…ne registry already serves

A host kernel hydrates every stored row into the engine registry, so an
echo there is never read by the catalog, and a delete's registry heal
re-registered it as a plain registry item: a deleted set, or an uninstalled
package's set, went on being served. The echo now registers only a name the
registry does not serve (the environment-kernel shape), and a delete also
removes a copy of our echo that leaked into the registry.

The boot test gains a delete and an uninstall leg on both kernels.

Claude-Session: https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 15 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/api/index.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/automation/flows.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/concepts/metadata-lifecycle.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/data-modeling/drivers.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/data-modeling/objects.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/deployment/cli.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/deployment/environment-variables.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/deployment/validating-metadata.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/kernel/cluster.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/kernel/contracts/metadata-service.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/kernel/services-checklist.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/permissions/authorization.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/permissions/permission-sets.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/plugins/packages.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v16.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/17-0.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/17-3.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/17-5.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/17-6.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/17-7.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))
  • content/docs/releases/v17/index.mdx (via sys_metadata (literal, a string literal in echoStoredSecurityCatalogOverlays))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 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 688a39d7700f3b3195e3bd782f42f25622cb4327 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1f309e9c934e4af38d4713dc03a49a7f9d7fc3e7 — the merge of head a966a51bc1013f3c738ce3b50140dfa414ddbe9c into base 688a39d7700f3b3195e3bd782f42f25622cb4327, 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 1f309e9c934e4af38d4713dc03a49a7f9d7fc3e7 && git checkout 1f309e9c934e4af38d4713dc03a49a7f9d7fc3e7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 688a39d7700f3b3195e3bd782f42f25622cb4327 a966a51bc1013f3c738ce3b50140dfa414ddbe9c && git checkout -B drift-repro 688a39d7700f3b3195e3bd782f42f25622cb4327 && git merge --no-ff a966a51bc1013f3c738ce3b50140dfa414ddbe9c

node scripts/docs-audit/affected-docs.mjs --json 688a39d7700f3b3195e3bd782f42f25622cb4327

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 688a39d7700f3b3195e3bd782f42f25622cb4327 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4051c1349a39050b6a947990f3dcd7138ac28654
Local-runs: none

Reviewed read-only against the brief's inputs: card #23308 (body, claim 6151102268, dispatch 6151126677, report 6154629520), the 7a blocker 6150594560 and measurement 6151042307 on #22975, PR #23258's body (09ef77c7d8), PR #23334 (body, file list, the three-dot diff against origin/main cc14657ede, the merge ref 8260e4f245), permission-set-projection.ts at S8's parent 99e944daf2, ADR-0131 D3/D4/D6 on origin/main, the WHICH LEVEL rule in pr-automation.yml, and the head's check-runs. Where a judgment needed a reader this PR feeds, I read it on origin/main: security-catalog.ts (core), resolve-authz-context.ts, protocol.ts (write-through, projector dispatch, delete heal, isArtifactBacked), metadata-manager.ts (registerInMemory, unregister), registry.ts (getItem), app-plugin.ts (the in-memory security collections) and the metadata-type table. The eight files in the PR's file list equal the three-dot diff and the merge ref's diff against main; the merge ref also carries five main commits the head predates (see ③).

① Derived judgments

Parity with S8's parent on an environment kernel.

  • The echoed body is the same shape. The parent's syncEvaluatorRegistry registered, under permission, the overlay body through stripDecorations (the same six decorations plus the marker; the function moved byte-for-byte), _provenance: 'org' only when tenantAuthored, _packageId only when tenantAuthored and the binding read answered, and _envProjection: true. echoSecurityCatalogMutation builds the identical object. tenantAuthored for a permission set is the same call, classifyPackagedPermissionSet(name, ql, { status: 'read', envelope }).status === 'org', over the same layered envelope; the owner is the same readServedPackageBinding (now typed) with the same warn-and-register-unowned fallback. On a packaged or unknown verdict the parent echoed an unstamped body; the new module skips the name when the registry serves it and otherwise registers it unstamped, the same as before. I found no permission set the new echo stamps org that the parent would not have.
  • Which names are echoed: the parent echoed every overlay-backed name; the new module additionally requires that the engine registry not serve the name. On an environment kernel the registry holds no stored row (applyRegistryWriteThrough returns at environmentId !== undefined, and hydrateOverlayIntoRegistry declines there), so for a metadata-door set the two conditions select the same names. The extra condition only removes echoes the catalog never read (registry first).
  • Two places the new echo serves a set the parent would not have, both in failure modes and both a stored, active overlay: (a) the parent gated the echo on the sys_permission_set upsert reporting seeded + updated + unchanged above zero, so a refused row write left the set unechoed; the new module has no row write and no such gate; (b) the parent's reconcile read sys_metadata through tryFind(..., 1000), so overlays past the thousandth row were never re-echoed after a restart; the boot pass reads without a limit (the engine's fillQueryAstDefaults adds none, and nothing in driver-sql caps it), so every active env-wide overlay is echoed. Neither serves a set the parent refused by policy; the overlay is the authoritative definition (ADR-0094 D1, ADR-0131 D3).
  • Positions: a new capability, judged below.

The dev's deviation.

  • The registry check: registry.getItem(type, name) answers the bare slot first, then any package-prefixed composite entry, so it is true for every stored row a host kernel hydrated and for every artifact a package registered in the engine registry. For permission that is the complete set of declared artifacts (manifest permissions are registry items). For position it is not: stack-declared positions are not registry items at all. The catalog's own measurement (header table in security-catalog.ts) counts 0 positions in the engine registry and 10 in the metadata service, and app-plugin.ts registers them there through registerInMemory('position', ...) in its SECURITY_FIELDS loop. So "a name the engine registry already serves" is the right test for a host kernel's stored rows and for declared permission sets, and an incomplete test for declared positions. Consequence next.
  • Shadowing a declared artifact: permission and position are both allowEnvironmentOverlay: false (metadata-plugin.zod.ts 1177-1178); D6's whole-item replacement is the five presentational types only, so no legitimate D6 overlay of either type exists. For a permission set the lock gate on the metadata door refuses an overlay of an artifact-backed name, and a legacy pre-rollback overlay was echoed unstamped on the parent but never read by the catalog (registry first): on both trees the catalog served the declared body, so nothing goes unserved and nothing is newly served as declared. For a position the door's seal, isSealedManagedItem, asks isArtifactBacked, which reads the registry's artifact items only, and no position authoring gate is registered (gates exist for object and permission). Nothing I read in the door refuses a metadata-door position save under a stack-declared position's name. If such a row exists on an environment kernel, the new echo registers it in the metadata service over the app's in-memory declaration (last write wins), the catalog then serves the row's permissionSets instead of the declaration, and the boot pass repeats that on every restart. The parent never echoed positions, so on an environment kernel the declaration kept winning. A host kernel already serves the row on both trees (the write-through hydrates the bare slot; registry first). This is the one place I can name where the change serves something on an environment kernel that S8's parent would have served as the declared item: a metadata author's overlay of a stack-declared position's binding list. The actor is a manage_metadata holder under single or the operator under a wall (ADR-0131 D3). Escalated as ③-1 with a guard that fits inside the new module.
  • A deleted set, or an uninstalled package's set: deleteMetaItem runs restoreArtifactRegistryView (which re-registers only on a control-plane kernel) and then awaits the projector with state deleted; the layered read has no overlay, current is our echo, dropEcho unregisters it. deletePackage deletes each row the same way (protocol 26948-26975), so no own uninstall code is needed. Boot-test leg 7 measured both on the environment kernel (resolve undefined, read refused). On a host kernel the echo never exists to leak: the write-through hydrates before the projector runs (22274 before 22299), and the boot restore (restoreMetadataFromDb, called from ObjectQLPlugin start, as the namespace module's cold-boot account states) precedes kernel:ready, so the boot pass skips every stored row there; the leak removal in dropEcho is a belt for the registry heal the dogfood run caught. A disabled (not uninstalled) runtime package: the catalog hides an item whose _packageId the registry reports disabled; the echo carries the binding; parity. A second replica of an environment kernel: the cluster replay (protocol 7607-7623) re-runs the write-through and the heal but no projector, so a save or delete on one replica changes nothing in another's metadata service until that replica restarts. That is the parent's behaviour too (projector-based); a deleted set stays served on the other replica until restart on both trees. NOT MEASURED.
  • Removing heal-to-declared: safe for permission, because every declared set is a registry item, is never echoed, and keeps being served registry-first after the overlay goes. Not safe for a stack-declared position: once the echo has overwritten the app's in-memory declaration, the overlay's delete makes dropEcho remove the only entry, and the position confers nothing until restart. That is a narrowing (fail-closed), and it is the other half of ③-1.

Positions.

  • The echo for position has the same shape as for permission, under the position type. The catalog's fromMetadata applies servable only: an object whose name matches, owner from _packageId, disabled-package rule. No type-specific shape test, so the reader accepts it; boot-test leg 5 proves the resolver reads it end to end (the holder is granted through the set the position names), and leg 7 that it survives a sibling's delete.
  • A position echo whose permissionSets names a set the catalog cannot resolve: positionBoundSetNames reads the names from the definition; catalogSetBody returns undefined for a name resolve('permission', name) does not answer, which "confers nothing"; the activation ledger filter (readDisabledCatalogNames) is applied independently. Fail-closed, no throw.
  • _provenance: 'org' is unconditional for a position. For a position row a code package materialized through the door (allowRuntimeCreate), that is the stamp a host kernel's hydrator applies to every stored row (stateTenantAuthorship), so it is parity with the host and display only.
  • Built-ins (everyone, guest) are registry items (declareEveryoneBaseline), registered at the earlier kernel:ready hook (security-plugin.ts 1741) ahead of the boot pass (4536), and never echoed; a metadata-door save under their names is skipped by the registry check.

The boot pass.

  • Selection: sys_metadata rows with type in permission, permissions, position, positions and state: 'active', read under seedCtx() (system, no tenant), then every row with a non-null organization_id skipped; the name set per type is run through the same mutation echo, so a name whose layered read has no overlay, or that the registry serves, is skipped again. Active, env-wide, the two types: correct.
  • Order: runBootstrap is the last kernel:ready hook SecurityPlugin registers, and dispatchHookPropagating runs handlers in registration order. The pass runs before reconcileAudienceBindingSuggestions at the end of the same function (4523). On the live path the awaited projector precedes emitMetadataMutation in save (22274, 22299, 22306), publish (23691, 23775, 23783) and delete (28185, 28210, 28216), so the [Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 listener's reconcile reads an echoed catalog. One caveat, identical on the parent: declareEveryone is a separate, earlier kernel:ready hook (1741 on the head, 1751 on the parent), so after a restart everyone is derived before the pass; an accepted runtime-package set is picked up at the next permission mutation. Fail-closed. The first request: the boot test's kernel.bootstrap() resolves after kernel:ready; whether a given host serves before kernel:ready settles is the server composition's property, unchanged by this PR and shared with the parent's reconcile.
  • Unreadable table: a throwing find is logged at warn with the consequence and the pass continues with the other type; boot continues and the catalog is partial for that type, each missing name fail-closed. The parent's tryFind swallowed the same failure as [] with no line at all, so this is louder. It is not a boot failure and not error; under AGENTS.md's one question (does the system look normal while something it claims persisted has not landed?) the answer is no, nothing is claimed persisted and the affected users are refused visibly, so warn is the level the rule assigns. The unit case pins the warn.

Prohibitions from the dispatch.

  • packages/metadata-protocol/src/protocol.ts is not in the file list.
  • No sys_permission_set write: the new module's only engine access is the sys_metadata read; the plugin diff adds no write; the edit-surface case pins engine.tables.sys_permission_set empty.
  • permission-set-projection.ts is present and unwired (no registerPermissionSetProjection or reconcilePermissionSetProjection call remains in security-plugin.ts); it imports isProjectionEcho, readServedPackageBinding and stripDecorations from the new module and nothing imports it but index.ts (unchanged exports), so 7a's deletion is a pure removal. No import cycle: the lock imports only @objectstack/metadata-core; organization-scope.ts imports nothing.
  • A1 was not built.

The pins.

  • The boot test wires nothing itself: it imports SecurityPlugin and appSecurityPluginOptions only, never the echo module; the environment legs assert anti-vacuity (registry.getItem falsy, source: 'metadata'), the refused-then-granted read, one pending everyone suggestion, the unowned no-package set, the position, a restart with the stored rows confirmed through an own better-sqlite3 connection, and the two retirements. The host control has no restart leg.
  • Ablations: reported by the dev (hook unregistered: 7 environment legs red, host green; boot pass removed: the restart leg and the leg after it red). No ablation case is committed, so the record weighs them as the dev's evidence. The restart leg's dependence on the boot pass follows from the code: the metadata service starts empty and nothing else registers these two types on an environment kernel.
  • fix(plugin-security): the permission-set echo names its runtime package, so an isDefault set records its everyone suggestion #23258's six cases all run on the new module. Case 3 (edit surface) now pins no sys_permission_set row instead of the row's managed_by: 'admin', which is the post-S8 truth, and keeps the lock, editable and deletable assertions; case 6 asserts the outcome registered instead of seeded: 1 and keeps the unowned echo and the single warn. The anti-vacuity assertions are retained. Not weakened.
  • The plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789 pair: the org stamp case is re-homed verbatim. The control changed from "a legacy overlay of a code-shipped set is echoed unstamped" to "a name the registry already serves is not echoed", which is a behaviour change the PR body discloses, and a narrowing of what the layered read's code layer shows; the catalog read the registry first on both trees.
  • New cases cover drop, skip, leak removal, drafts and foreign types, the boot pass's selection (plural spelling, org rows skipped) and its unreadable-table warn. The declared-position control registers its position in the engine registry, which is not the shape a stack declares (metadata service); see ③-1.

Published text.

  • Changeset: accurate; "they change nothing a host kernel resolves" holds because host hydration precedes both the projector and the pass. "In the security catalog again" is true of permission sets; the next paragraph states positions never were, so the text is not misleading.
  • Module JSDoc: "A position is always tenant-authored at that point, because only names no artifact declares are echoed" is false for a stack-declared position, which lives only in the metadata service and is not seen by the registry check (③-1). Everything else in the header matches the code.
  • permission-set-projection.ts: the replacement comments are accurate; the module's own header still describes the retired row projection, which 7a deletes.
  • security-plugin.ts 4349-4355 names the echo as the environment-kernel path: accurate.
  • Docs: content/docs/permissions/system-context.mdx row 11 (the write-through is no longer registered) stays true; no hand-written page states the environment-kernel behaviour either way; ADR-0094, 0126 and 0131 anchor the projection file, which is kept.

② Semver level

@objectstack/plugin-security patch, Clause-②: no, agreed. packages/plugins/plugin-security/src/index.ts is not in the file list; the new module's exports are not re-exported from the entry; readServedPackageBinding gains an optional third parameter but was module-private before and stays off the entry. No new accepted key or value: the position type and PositionSchema.permissionSets exist already, and the change is which kernel serves a stored row. Under WHICH LEVEL a fix( that changes no public surface stays patch; the position capability is behaviour on one topology, not a published surface, so the dispatch's "unless your measurement finds a surface S8's parent did not have" is not triggered. With no declared, check-changeset-no-major's level axis stands down, so the level is this record's to judge, and it stands.

③ Boundary flags

  1. Escalated to the seat, act before the queue. A metadata-door position row whose name a stack declares is now echoed over the app's in-memory declaration on an environment kernel, and the boot pass repeats it each restart; the overlay's delete then removes the declaration until restart. The door does not refuse such a row (isArtifactBacked reads the registry's artifacts; no position gate), and a host kernel already serves it, so the new behaviour is host parity, not a new door, and the actor is a metadata author. It is still the one way this change serves, on an environment kernel, a definition S8's parent would have served as declared, and the module's own JSDoc premise is false for it. Recommended, inside the dispatch's file surface: in echoSecurityCatalogMutation, treat a metadata-service item of the name that is not our echo as declared and skip (the mirror of registryServes), with a pin whose declared position is registered through the metadata service rather than the engine registry. If the seat prefers, the same goes to a card against the door (protocol.ts, seat 1's region) and this PR lands as is; either way the dev's JSDoc line should be corrected.
  2. dropEcho on the registry-serves branch calls metadata.unregister while the overlay row still exists; the inline comment "the stored row is already gone" is true only on the delete branch. MetadataManager.unregister deletes through loaders whose protocol is datasource: with write capability. In both compositions reviewed none is registered: NodeMetadataManager carries a FilesystemLoader and the plugin's MemoryLoader (memory:), and setDatabaseDriver fires only when the manager is constructed with both datasource and driver, and skips itself on an environment kernel. Latent, not live, and it also announces a spurious deleted watcher and realtime event for a name that is still served. Recommend an in-memory-only drop on that branch or a corrected comment. Not blocking.
  3. Deviations, each answered: (a) registry-served names are not echoed, accepted for permission, incomplete for position (item 1); (b) heal-to-declared removed, accepted for permission, the narrowing half of item 1 for positions; (c) the plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789 control replaced, disclosed, a narrowing of the layered read's display only.
  4. The dev lists no open questions. Mine is item 1's control shape.
  5. NOT MEASURED: a real cloud kernel (the factory file is outside this repository, as the dev's measurement states); a host-kernel restart with the hook wired (no host restart leg; by ordering the boot pass is a no-op there); a multi-replica environment kernel (the echo is per-process and the cluster replay runs no projector, parity with the parent); the ablations (dev-reported); dogfood and the @objectstack/cli integration tier as the dev's local runs, since PR CI skipped the dogfood shards (skills(ci): two-speed CI — fast set on PR and merge-queue runs, full battery hourly on main and before a release, main-red stops the lane, four required contexts plus the guard, timing gates report-only, test budgets not deletions #22998) and whether Test Core executes the cli integration project was not verified here; the five main commits between the dev's last merge (87e902dcbb) and origin/main (cc14657ede), which are in the merge ref but not in the dev's local gate runs; one of them (37510aa0f9, ADR-0138 guest grants) edits security-plugin.ts in other regions, merges cleanly, and folds no positions for a guest, so it does not read the echo.
  6. Check-runs on the head, polled at 2026-10-13T20:47Z: 32 completed success, 6 skipped (Build Docs, Console Pin Gate, the dogfood shards, Dogfood Verify CLI, the packed-tarball smoke, Temporal Conformance), 1 still running: Test Core (3/6). Their conclusions are the gate verdicts; this record does not pre-judge the running shard.
  7. Out of scope, found: the metadata door's D6 seal cannot see a metadata-service-declared position (pre-existing on host kernels; protocol.ts). The everyone derivation at the earlier kernel:ready hook runs before the boot pass on both trees (fail-closed until the next permission mutation).

Adopted by domain:engine seat 2 at 2026-10-13T20:50Z, after a tool-use audit of the isolated reviewer's transcript. The reviewer made reads, git fetch into review refs, REST GETs, and record writes only. It made no GitHub write and ran no build, test or gate. The seat takes the verdict as PASS, and the PR is held for one small patch round before the queue:

  • ③-1 is fixed in this PR, with the reviewer's recommended remedy. echoSecurityCatalogMutation skips a name the kernel's metadata service already holds as a non-echo item: a stack-declared position, for instance, which the registry check cannot see. A pin registers a declared position through the metadata service and shows that an overlay neither shadows it nor deletes it. On this authorization path, deleting an overlay must never take away a declared item until restart.
  • ③-2 is fixed in the same round: the drop is in-memory only, so no datasource: delete and no spurious deleted event.
  • The JSDoc premise ("a position is always tenant-authored") is corrected.
  • The Test Core (3/6) shard that was still running is read again on the new head.

Implemented-by: claude/issue-23308-security-catalog-echo
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

…claration the metadata service holds, and removes nothing while the stored row exists

A name the kernel's metadata service already holds as something other than
our echo (a stack-declared position, which a composition can declare in the
metadata service only) is not echoed: the echo would overwrite the
declaration, and the overlay's delete would then remove it until restart.
The boot pass takes the same path.

On a name the engine registry serves, an earlier echo is now left in place
instead of being dropped: the metadata service's one removal, unregister,
also deletes through its writable datasource loaders and announces a deleted
event, which is wrong while the stored row still exists. The overlay's
delete drops it.

The module doc no longer claims every echoed position is tenant-authored
because no artifact declares it. The boot test gains a stack position whose
name a stored row took before the stack declared it.

Claude-Session: https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a966a51bc1013f3c738ce3b50140dfa414ddbe9c
Local-runs: none

Delta review of PR #23334 from the previous reviewed head 4051c1349a to a966a51bc1, read-only against the brief's inputs: the previous record 6154942355 (its ③-1, ③-2 and the seat's adoption note), card #23308 (body, dispatch 6151126677, report 6154629520, and the later comment 6156567934, which is seat 1's overlap note and not an os-dev-report; the card carries no later report), PR #23334 (body, the delta, the net diff against origin/main 688a39d770, the merge ref 1f309e9c93), the merged main commits 430e5f5ad1, 14a13bce06, 37510aa0f9 and 572ecfb2ef (the only other commit in the merged range touching metadata-protocol; no other touches plugin-security), and the head's check-runs. Where a judgment needed a reader the PR feeds, I read it on origin/main: security-catalog.ts (core), resolve-authz-context.ts, protocol.ts (the projector sites, isArtifactBacked, isSealedManagedItem, the delete heal), metadata-manager.ts (registerInMemory, get, unregister, invalidateForForeignWrite), builtin-positions.ts, app-plugin.ts, engine.ts (the stack-collection list) and security-catalog-namespace.ts.

The delta is 18 commits: one of the branch's own, 22813f7459 (three files: the echo module, its unit suite, the boot test; 129 added, 30 removed), and two merges of main (2f3ea34fc5, a966a51bc1). origin/main 688a39d770 is an ancestor of the head, so the merge-base is main itself: the net diff is the same eight files as before (the changeset, the new module, its suite, the renamed-away suite, the projector, security-plugin.ts, the boot test, the engine-double ledger), and the merge ref's diff against main equals it. index.ts and protocol.ts are not in it.

① Derived judgments

③-1, the stack-declared position.

  • The guard. After the registry check, echoSecurityCatalogMutation now answers skipped when metadata.get(type, name) returns an item that is not our echo (module line 217). MetadataManager.get reads the in-memory store first and then the loaders, so a stack's registerInMemory declaration (app-plugin.ts, the SECURITY_FIELDS loop) and a file-declared item are both seen; the boot pass calls the same function, so it skips too. On the delete branch the rule that was already there (line 201: nothing is dropped unless current is our echo) leaves a declaration alone. Whatever non-echo item get answers is exactly what the catalog's fromMetadata serves for the name (the same get), so the skip never leaves a name unserved; it decides that the declaration, not the stored row, is what an environment kernel serves. That is the mirror of registryServes the previous record asked for.
  • The unit pin (security-catalog-echo.test.ts, the case "a STACK-declared position (held by the metadata service only) is neither shadowed by an overlay of its name nor removed by its delete"). The declared position is registered through makeMetadataService().registerInMemory; the engine is engineDeclaring() with no artifacts, so registry.getItem is not what skips it. The case asserts skipped on an active save, 0 from a boot pass whose find returns the row, skipped on the delete, and object identity of the declaration after each step. With line 217 removed, the active save falls through to registerInMemory of the overlay body and the identity assertion reddens. Anti-vacuous.
  • The dev's two measurements. (a) "In the boot composition a stack-declared position is also in the engine registry": consistent with engine.ts ([finding] an environment save of a position over a package-declared position name answers 200 and wins the by-name read; a permission set's is refused 403 NOT_OVERRIDABLE #22203). positions is in the engine's stack-collection list, and registerMetadataCollections registers each entry under position with the stack's package id (registerItem(pluralToSingular(key), stampPackage(...), 'name', ownerId)). The previous record's contrary reading rested on the catalog header's table, measured at 3d9188502e, which predates [finding] an environment save of a position over a package-declared position name answers 200 and wins the by-name read; a permission set's is refused 403 NOT_OVERRIDABLE #22203; builtin-positions.ts restates the old fact for the plugin-manifest seam, a different registrar. (b) "The door refuses a save under a declared name": refusePackagedBaseOverride asks isSealedManagedItem, which is isArtifactBacked (a registry artifact carrying a genuine package id) and position opens no environment overlay, so on that composition a save under a name the running stack declares is refused. True there. In a composition where positions reach the metadata service only (the artifact-door registrar; I did not trace whether it also reaches the registry), the door does not refuse, and the new guard is what keeps the declaration served on an environment kernel; a host kernel serves the hydrated row there on every tree (the previous record's out-of-scope item 7, unchanged).
  • The boot leg. The environment kernel boots the stack without the position, stores a row under the name (label Overlay, permissionSets: []), restarts with the stack declaring it (label Stack position, permissionSets: [DECLARED_SET]), and asserts the declaration's label and binding list and the holder's granted read; then it deletes the row and asserts the same again. Were the stored row served, the holder would be refused and the label would differ, so the leg is anti-vacuous. It covers the one order the door allows (row first, declaration later). As the dev notes, on this composition the registry check is what skips, not the new guard, and the source is not asserted. The namespace rule (security-catalog-namespace.ts) cannot refuse that redeploy on an environment kernel, because it reads the registry's bare slot, which an environment kernel never fills; so the path is reachable and the leg is the right one.

③-2, the drop.

  • The dev's finding holds. MetadataManager has one removal, unregister: it deletes through every datasource: loader with write capability, drops the registry entry, publishes a realtime deleted event and notifies watchers. unregisterPackage calls it. invalidateForForeignWrite drops the registry entry alone but is protected and off the service contract. The core fallback memory-metadata.ts has an in-memory unregister. So "in-memory only" is not available on the real service, and the round's answer is to call unregister only once the stored row is gone: the registry-serves branch no longer drops (line 213), and dropEcho's comment is true on its one remaining caller, the delete branch.
  • Can that leave a stale echo serving a deleted or revoked item? Traced:
    • A delete on an environment kernel. deleteMetaItem runs the projector with state: 'deleted' on both of its exits (protocol 27743 and 27918); the module skips the layered read, current is our echo, dropEcho unregisters it and removes any copy leaked into the registry. Boot leg 7 measures the resolve going undefined and the read refused. The revertCommit soft-remove limb (26568) sends the same deleted event.
    • An uninstall of a runtime package. deletePackage deletes each row through deleteMetaItem; boot leg 7 measures it. An uninstall of a code package removes registry artifacts, not rows, and no echo of an artifact exists (registry first).
    • A rollback, and the revertCommit restore limb (27046, 26682), send active; the module ignores evt.body, re-reads the layered overlay and re-registers, so the echo follows the restored version.
    • A restart. The metadata service starts empty, and the boot pass reads state: 'active', env-wide rows only; a deleted row is not re-echoed.
    • A host kernel. No echo ever exists there (the write-through hydrates before the projector in save and publish; loadMetaFromDb precedes kernel:ready), so the no-drop rule changes nothing. The heal's re-register tier is gated environmentId === undefined, and on a host kernel it has no echo baseline to re-register. The leak belt in dropEcho stays.
    • The branch the round changed, an echo coexisting with a registry-served name, is reachable on an environment kernel only by a hot install of a package declaring an echoed name (the namespace refusal reads the bare slot, so it does not fire). While the artifact is in the registry the echo is unread (registry first), a save under the name is sealed, and a delete of the row drops the echo. After that package's uninstall the catalog falls back to the echo, which is the unchanged stored row, the body a host kernel would serve from its bare slot. No deleted or revoked item is served. NOT MEASURED end to end; the unit case at line 372 pins the abstract sequence (no removal while the row exists; dropped on the row's delete; the artifact stays).
    • A disabled runtime package. The echo carries _packageId from the binding when tenant-authored, and servable hides a disabled owner's item. Unchanged.
    • A second replica. Parity with the parent (a per-process echo; the cluster replay runs no projector). NOT MEASURED, as before.

The JSDoc is corrected. The header no longer says a position is tenant-authored because no artifact declares it; it states both skips (the registry, the metadata service) and why unregister is called only once the row is gone. The inline comments at 206 to 217 match the code.

The merged main commits.

  • 14a13bce06 (the save, restore and publish doors refuse a body packageId that disagrees with the binding; declaredOwnerOf reads the binding only). The echo's owner is the binding too: readServedPackageBinding reads _packageId off the protocol's by-name read, which states it from the row's package_id column, and the catalog takes an entry's owner from _packageId alone (security-catalog.ts 181). The stripped overlay body keeps a spec packageId key, which no reader of the echo uses for ownership. Not redundant: on an environment kernel the echo's _packageId stamp is the catalog's only owner source; the refusal makes the body key agree with the binding, it does not supply the stamp. Not contradictory: a pre-refusal row whose body key disagrees is echoed under its binding, which is how that commit says such a row is read. The unit case "the security catalog and the protocol's by-name read name the same owner" pins the agreement. No shared file.
  • 430e5f5ad1 (under a wall every environment-metadata write is the operator's). It edits the runtime doors (meta.ts, packages.ts, automation.ts, activation-gate.ts, meta-write-capability.ts), not the protocol or plugin-security. Under a wall the actors whose rows the echo can serve narrow to the platform operator; the echo sees the same landed rows and the catalog serves the same bodies. No interplay.
  • 37510aa0f9 (the guest's grants channel). The guest's sets are the guest anchor's permissionSets, read through catalog.resolve('position', 'guest') (resolve-authz-context.ts 1140). guest is a built-in that registerBuiltinPositions declares in the engine registry at start() under the plugin's package id, so registryServes is true for it and a metadata-door position named guest is never echoed (the door seals it besides). What the echo can add on an environment kernel is the resolvability of a set the anchor already binds, which is the admin's acceptance under ADR-0090 D5 and what S8's parent served; ruling C confines the guest's rows to share links regardless. The commit's security-plugin.ts hunks (the imports, 1454, 6463, 8679, 8715) are outside the echo's region (4360 to 4490); the guest-fold removal reads no position for a guest.
  • 572ecfb2ef (protocol.ts, the v18 stored preview). No projector, registry or metadata-service path. No interplay.
  • Merge. The head contains origin/main 688a39d770; the API reports mergeable: true with mergeable_state: blocked (a draft with required checks running); the merge ref carries nothing beyond the head. Whether the pins hold on today's main is CI's to say (③).

Everything else the previous record judged still holds on this head: parity with S8's parent for permission (the same body, the same lock verdict, the same binding read; the extra skips remove only echoes the catalog never reads), the prohibitions (protocol.ts untouched; the module's only engine access is find('sys_metadata'); the projector unwired and importing from the new module; A1 not built), the boot test wiring nothing itself, the 18 unit cases including the six #23233 cases and the #21789 pair, the changeset text, security-plugin.ts's once-guards, and the hook order (declareEveryone at 1751 ahead of runBootstrap at 4608).

② Semver level

@objectstack/plugin-security patch, Clause-②: no, unchanged and still right. The round adds no export (index.ts is not in the net diff; the module stays off the entry), no accepted key or value, and narrows behaviour on one topology (a declaration wins over a stored row on an environment kernel). A fix( with no public surface stays patch; the dispatch's "unless your measurement finds a surface S8's parent did not have" is not triggered.

③ Boundary flags

  1. Act before the queue (procedural, not a code finding). The patch round has no os-dev-report: report 6154629520 and the PR body's Verification section both name 4051c1349a, and the body's "What this changes" still says only "a name the engine registry does not already serve", nothing on the metadata-service skip or the no-drop rule the round added (the commit message carries both). PR CI runs neither dogfood (skills(ci): two-speed CI — fast set on PR and merge-queue runs, full battery hourly on main and before a release, main-red stops the lane, four required contexts plus the guard, timing gates report-only, test budgets not deletions #22998) nor the derived sweep, so the dispatch's dogfood and dispatch-gates --ran have no run on record for 22813f7459 or this head. The risk the round adds to those is low by construction (on a host kernel, dogfood's composition, the write-through hydrates every row first, so the new guard is unreachable and no echo exists for the no-drop rule to keep), but low is not reported. Ask the dev for the round's report on this head (dogfood, the derived sweep with --ran reconciled, the plugin suite and the boot test) and for a body that states both changes and the verified head.
  2. Seat 1's overlap note 6156567934 at 2026-10-13T22:28Z: PR 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 re-keys the write-through skip to "the registry takes no stored rows". It touches no file of this PR. After it lands, a hydrating environment kernel serves its rows from the registry and registryServes makes the echo a no-op there, which is the intended split; the boot test header's sentence about environmentId goes stale then, as the note says. The later lander merges main and re-runs the other's pins; one thing to read at that point is the boot order on a hydrating environment kernel (hydration against kernel:ready): a boot pass that ran before hydration would register an echo the bare slot then shadows, unread and dropped on the row's delete. Not this PR's defect. NOT MEASURED.
  3. NOT MEASURED, carried or new: a real cloud kernel (its factory is outside this repository); a multi-replica environment kernel; the hot-install sequence in ③-2's last trace; the artifact-door registrar composition's registry state for positions; dogfood, the derived sweep and the @objectstack/cli integration tier on this head (the dev's runs are on 4051c1349a; whether Test Core executes the cli integration project is still not verified here); the ablations (dev-reported, on the earlier head).
  4. Out of scope, found, unchanged since the previous head: an unknown lock verdict (every provenance source failed to answer) leaves a permission echo unstamped and so unowned, which the disabled-package rule cannot see; parity with the parent. A deleted event carrying an organization id drops the env-wide echo without a layered read; fail-closed until the next save or boot, and org-scoped rows of these two types are no supported shape.
  5. Check-runs on the head, polled at 2026-10-13T22:39Z: 30 completed success, 6 skipped (Build Docs, Console Pin Gate, the dogfood shards, Dogfood Verify CLI, the packed-tarball smoke, Temporal Conformance), 3 still running: Test Core (2/6), (3/6) and (4/6). Their conclusions are the gate verdicts; this record does not pre-judge the running shards. The docs-drift advisory 6154624465 lists pages anchored on the sys_metadata literal only; no hand-written page states the environment-kernel behaviour either way, as before.

Adopted by domain:engine seat 2 at 2026-10-13T22:43Z, after a tool-use audit of the isolated reviewer's transcript. The reviewer made reads, git fetch into review refs, REST GETs, and scratch and record writes only. It made no GitHub write and ran no build, test or gate. The seat takes the verdict as PASS. Before the queue, the seat waits for the round's os-dev-report on this head, with dogfood and the derived sweep, and for the three running Test Core shards. The seat updates the PR body itself (the dev role makes no body PATCH), naming a966a51bc1 and the round's two behaviour changes. Seat 1's overlap note on PR #23345 (no file overlap) goes to whichever of the two lands second.

Implemented-by: claude/issue-23308-security-catalog-echo
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Merged via the queue into main with commit 6d96721 Oct 14, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23308-security-catalog-echo branch October 14, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants