Repository navigation
feat(core): one by-name read of the security catalog (ADR-0131 C2, stage S1) - #22091
Conversation
…registry and the metadata service ADR-0131 D2-D4: positions, permission sets and capabilities resolve by name from the environment registry. No single in-process reader holds the whole catalog (the engine registry has no stack-declared position; the metadata service lacks the platform permission sets), so the read is their union in one order, registry first. No consumer is switched. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
… ship Today's answer, until shared catalog names are ruled: the first-registered package's body with no stored override; the override one package stored for itself, for every caller, once hydrated. The metadata door's by-name read is asserted beside each answer. A position name two stacks declare shares one metadata-service slot; the later registration holds it. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…ures Per type, the read lists exactly the declared names (the showcase stack's positions, permissions and capabilities, plus the platform's bootstrap permission sets) and exactly the names the metadata door lists, in single, single with the Default Organization, and walled postures; every listed name resolves by name to the entry the list gave. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
The row's metadata column is text; passing the object added a type error the test-typecheck ledger does not record. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin afff2169134a217a9b25e2456ddc9c3d7c19062a && git checkout afff2169134a217a9b25e2456ddc9c3d7c19062a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bafb58bb08aa3989b1d72a633496b361fcf0842e e05d4692103c4d8276a5aecafb01e188158d6f5c && git checkout -B drift-repro bafb58bb08aa3989b1d72a633496b361fcf0842e && git merge --no-ff e05d4692103c4d8276a5aecafb01e188158d6f5c
node scripts/docs-audit/affected-docs.mjs --json bafb58bb08aa3989b1d72a633496b361fcf0842e
|
Contract reviewServed-tier: ① Derived judgmentsInputs: card #15196 (body and all 17 comments), PR #22091 (body, the 6-file list, the net diff against Public surface of Accept-set of the new read, each judged on the diff:
No consumer switched, verified on the diff: the only non-test source lines are the new module and one export block. The pinned sibling checkout is unaffected, since the change is an additive export. ② Semver level
③ Boundary flagsDev flags (the PR's Acceptance notes and the os-dev-report
Card-level open questions: Q3 (the walled half) and Q4 remain with the maintainer ( Gate verdicts on the head: 30 check-runs Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Regen-provenance: 6041388058 · Seat Why the merge: the first CI and Lint & Type Check runs on What the merge carries: the PR's own delta against Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37654973228 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Part of #15196
Clause-②: yes (widening)
The new public surface of
@objectstack/core. One value export,createSecurityCatalogReader(sources), re-exported from the root barrel throughpackages/core/src/security/index.ts. It takes{ registry, metadata }(typeSecurityCatalogSources) and returns aSecurityCatalogReaderwith two members.resolve(type, name)returns a promise of theSecurityCatalogEntrythat the name resolves to, orundefinedwhen no reader holds it.list(type)returns a promise of an array ofSecurityCatalogEntry, one per name, each equal to whatresolveanswers for that name.typeisSecurityCatalogType, the union of'position','permission'and'capability'. Any other value rejects with aTypeError. An empty or non-string name resolves toundefined. An entry is{ type, name, definition, source, packageId? }.definitionis the reader's own definition object, read-only and shared.sourceis aSecurityCatalogSourceName,'registry'or'metadata'.packageIdis the definition's_packageId, present when the definition names one. The types exported with it areSecurityCatalogType,SecurityCatalogSourceName,SecurityCatalogRegistry,SecurityCatalogMetadataService,SecurityCatalogSources,SecurityCatalogEntryandSecurityCatalogReader.SecurityCatalogRegistryneedsgetItem(type, name),listItems(type)andisPackageDisabled(packageId); ObjectQL'sSchemaRegistrysatisfies it as it stands.SecurityCatalogMetadataServiceneedsget(type, name)andlist(type), plus the optionalgetDiagnosedandlistDiagnosed; theMetadataManagersatisfies it as it stands. Construction throws aTypeErrorwhen either source lacks a member the read calls. The read rejects withAuthzStoreUnavailableError(SERVICE_UNAVAILABLE, 503) when a reader throws, when a metadata by-name read lost a loader and found nothing, or when a metadata list lost a loader at all. Nothing calls the reader yet, so no grant changes.What this stage is
Stage S1 of the C2 stage plan (ADR-0131 D2–D4): one seam that later stages switch readers onto. No consumer is switched. Files:
packages/core/src/security/security-catalog.ts(new): the read.packages/core/src/security/index.ts: the export, one block.packages/core/src/security/security-catalog.test.ts(new), a new describe block at the end ofpackages/objectql/src/protocol-boot-hydration-scoped.test.ts, andpackages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts(new)..changeset/15196-core-security-catalog-read.md:minorfor@objectstack/core.The reader per type, measured before choosing
Re-measured on objectstack
mainat3d9188502e(the census was taken ate67ba80049; the readings are unchanged). Showcase was booted four ways:single,singlewith the Default Organization, walled with the real organizations package declared by a temporary host root, and walledposture-only. Names per reader, identical in all four boots:SchemaRegistry.listItems)list)GET /api/v1/meta/:type)positionpermissioncapabilityNeither in-process reader holds the whole catalog. The engine registry has no stack-declared position, because the engine's stack-collection list does not decompose
positions. The metadata service lacks the eight platform permission sets thatplugin-securityships on its own manifest (admin_full_accessamong them). The door is a serving surface: it reads stored rows on each request and decorates what it serves. So the read is the union of the two in-process readers, in one order: the engine registry first, then the metadata service for the names the registry does not hold. The reader that answers on showcase, per type:permission: the engine registry (17 of 17).position: the metadata service (10 of 10).capability: the engine registry, which answers first. Both hold the same 2.For the 11 names both readers hold (9 permission sets and 2 capabilities), the definition bodies are identical across the registry, the metadata service and the door's by-name read. That was measured by comparing every non-underscore key. So the order changes no body on showcase.
What the read does and does not answer
activeflag stays authoritative, and no definition carries it. An entry says that a definition exists under a name, never that it grants. A later stage that reads definitions keeps reading the row flag withisRowActive. The module doc says so in those words.listItemsalready hides such items, and the by-name read applies the same rule, so the two members cannot disagree. This is the one rule the read adds on top of the readers' own answers; see the first Acceptance note.Pins, each ablated and restored by blob
Every ablation went through
scripts/ablation-replace.mjsin WRAP mode, so its anchor hit exactly once and the blob changed. The restore was proven with "blob == HEAD andgit diff HEADis empty". The dist-resolved legs also rebuilt the package, proved the marker withscripts/ablation-dist-preflight.mjs(marker present in 2 built files), and proved it gone after restore (marker absent from all … built files,working tree clean against HEAD).security-catalog-showcase.dogfood.test.ts(31 tests, green). For each type in each of the three postures (single,singlewith the Default Organization, walledposture-only), the read lists exactly the declared names. The expected names come from the producers, never from a registry: the showcase stack'spositions,permissionsandcapabilities, plussecurityDefaultPermissionSets. The read also lists exactly the door's names, and every listed name resolves to the entry the list gave.plugin-security's manifestpermissions:entry was replaced with an empty array, andplugin-securitywas rebuilt. Red in all three postures:permission: lists exactly the declared names, withadmin_full_accessandorganization_adminmissing (expected 17, received 15). The door assertion stayed green because the door lost the same names, which is why the expected sets are derived from the producers.protocol-boot-hydration-scoped.test.ts(5 tests, green). It uses the realSchemaRegistry, the realloadMetaFromDbhydration and the file's existing pinned engine double, for bothpermissionandposition. With no override, the read returns the first-registered package's body. With an override bound to package B, it returns B's override andpackageIdB. The door'sgetMetaItemis asserted beside each case. A position name that two stacks declare shares one metadata-service slot, and the later registration holds it.permission,position).security-catalog.test.ts(14 tests, green), each ablated in the core source:a name both readers hold is answered by the engine registryandlist gives one entry per name ….a registry read that throws.a metadata read that throws.a metadata read that lost a loader and found nothing.construction refuses a missing reader ….codeSERVICE_UNAVAILABLE,status503, and the brand), never a bare throw.The ablations ran at
0482468f8e. The two later commits add the changeset and turn one fixture argument into JSON text. Neither touches an ablated line or assertion.Verification, at head
b70dd032bepnpm --filter @objectstack/core test:Test Files 78 passed (78),Tests 2192 passed (2192). This ran at46fe89abad; the last commit changed only an objectql test file.pnpm --filter @objectstack/core typecheck: exit 0, test layerOK, debt unchanged. The new test file is in thetsconfig.test.jsonprogram (checked with--listFiles).pnpm --filter @objectstack/objectql typecheck: exit 0, test layerOK.pnpm --filter @objectstack/dogfood typecheck: exit 0.Tests 8 passed (8). The P1.1 file (--project isolated):Tests 31 passed (31).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwas re-derived on this change: 70 families, plus the 2 the dispatch named that the re-derivation does not (check:authz-resolver,check:dispatcher-error-vocabulary). All 72 were run with the exit code captured before any pipe, and all exit 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: 7 unbuilt packages); after building them it exited 0, with106 published require entry point(s) across 66 package(s) load.--ranreports70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED. Some verdict lines:check-engine-double-contract: OK — 980 pinned,check-nul-bytes: OK,check-test-source-alias OK,check:authz-resolver: single shared authorization resolver intact,check-adr-0087-registration: … 1 non-breaking changeset(s) seen,This diff introduces no major bump.eslint --no-inline-config --format jsonon the 5 changed.tsfiles reports 5 files, 0 errors, 0 warnings, and every file matches the config.eslint.config.mjsenables no type-aware linting (noparserOptions.project). The only files it reads are two baselines this diff does not touch, so no untouched file's verdict can move. The repository-widepnpm lintis CI's.Acceptance notes
listandresolvedisagreeing about whether a name exists, because the registry'slistItemsapplies the rule unconditionally._packageIdin the metadata service, so the disabled-package rule cannot reach them. This is the same root as the positions being absent from the engine registry.scripts/check-stack-collection-maps.mjs's waiver forpositionssays positions "reach the registry through the security bootstrap". Measured, they reach the metadata service and the catalog rows, never the engine registry.scripts/adr-anchors/, because the stage's surface is the module and its export. ADR-0131 is cited in the module.posture-only. The real organizations package resolves only from a host root that declares it, and this suite's root does not. The one-off reading with a declared host root gave the same sets.Generated by Claude Code