Uh oh!
There was an error while loading. Please reload this page.
feat(cli): build the cross-producer sys_secret reference union (#12663) - #12754
Conversation
…cret-reference-union
…cret-reference-union
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 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 72cc0ed6360062e0def30a2eab358a85037800dc && git checkout 72cc0ed6360062e0def30a2eab358a85037800dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 168941cea851ca77a42ce5db01b77f1c91273bee 929d12c698b030d53869aaacdd4ffc89d2f4de1d && git checkout -B drift-repro 168941cea851ca77a42ce5db01b77f1c91273bee && git merge --no-ff 929d12c698b030d53869aaacdd4ffc89d2f4de1d
node scripts/docs-audit/affected-docs.mjs --json 168941cea851ca77a42ce5db01b77f1c91273bee
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12663
Builds the cross-producer
sys_secretreference union — the precondition the deletion half of #8103 is blocked on. Read-only across all three producer surfaces; no producer needed a change, and this PR contains no deletion command, no dry-run, no sweep and no classifier.packages/cli/src/utils/secret-reference-union.ts— the unionpackages/cli/src/utils/secret-reference-union.test.ts— 20 pins.changeset/sys-secret-cross-producer-reference-union.md— patch,@objectstack/cliThe premise, reproduced against real code
Ruling 5 asked for the #8103 measurement to be driven rather than repeated. It reproduces.
the premise: the shipped settings-scoped classifier calls a LIVE credential 'orphaned'boots a real ObjectQL engine, the realLocalCryptoProvider, the real datasource secret binder and the real shippedclassifySysSecretRows, and shows a live, engine-owned credential classifiedorphaned:smtpwith asecret-typed fieldpasswordmints itssys_secretrow withnamespace = smtp,key = password(that isencryptSecretFields, verbatim);smtpwith an encrypted specifierpasswordproduces the identical attribution pair;orphaned.Attribution by (namespace, key) is a name match, not ownership, exactly as the card says. In the same fixture the datasource handle answers
unattributableonly becausedatasource/mainhappens not to collide — the escape is luck, not a guard. The union names all three handles, which is what makes the deletion predicate decidable.Completeness is structural, not asserted
An incomplete union is strictly worse than no union, so two devices carry it rather than a claim in a comment:
buildSecretReferenceUniontakes aRecordkeyed on the family union, so omitting a producer is a type error rather than a smaller union.gap; the union then reportscomplete: falseandassertSecretReferenceUnionCompleterefuses it with the ADR-0112 pairPRECONDITION_REQUIRED/ 428.undefinedand[]are deliberately different answers for the host's code-defined datasources: "nobody answered" versus "there are none".Two design decisions worth reviewing explicitly:
getDriverForObject(). Thesecret:ref only exists at that layer — the read path masks it unconditionally, which is why the engine's own privileged verbs read there too. A positive control pins this:the driver-level read is load-bearingshows the generic engine read returningSECRET_MASKwhile the union still finds the handle.limitis present, so an unbounded read returns the whole holder set.The acceptance condition: each family separately ablated, each reds a named pin
Ruling 4's criterion is falsifiable, so it was falsified rather than asserted. Each leg injects a silent early return into exactly one collector — the dangerous shape, since the union still reports
complete: true— proves the mutation on disk by marker count and blob hash, runs the suite, then restores and proves the restore by blob hash plus an emptygit diff HEAD. No build is involved:packages/cli/distdoes not exist in this worktree, so the test's relative import resolves to the TypeScript source.family 1 — settings ... names the handle held ONLY by sys_setting.value_encfamily 2 ... names the handle held ONLY by a business rowfamily 3 ... names the handle held ONLY by a datasource artefactEach family pin asserts a handle only that family holds, and also that exactly one reference names it — so the coverage cannot be borrowed from a sibling family.
Family 2 carries the extra pin the card asks for, because it is the one family that cannot be precomputed:
registers a new secret field at runtimeregisters a tenant-shaped object after the first union is built, writes a row through the same producer path, and shows the new handle arriving with no code change — with the before-state asserted absent so the pin cannot pass vacuously.Placement — measured today, not cited
The card's numbers are a 2026-08-26 reading and it says so. Re-measured on this checkout:
package.jsonfiles, not 73. The count moved; the answer did not.@objectstack/cliand@objectstack/verify. Unchanged from the card.@objectstack/dogfoodjoins, but only by depending on those two, so it is downstream of both rather than an independent candidate.Chose
@objectstack/cli. Both candidates are in this lane, so no cross-lane coordination either way.@objectstack/verifyis a published boot-and-verify harness whose charter is CRUD round-trip fidelity and the RLS invariant; an operator-safety primitive does not belong inside it. The named reader — #8103's deletion command — is inpackages/cli, andsrc/utils/is where this package already keeps read-only diagnostics over a booted runtime. Landing here also needed nopackage.jsonedit, which the fence forbids.Not exported from the package entry barrel. The named reader is in this same package and imports it relatively; publishing an external API with no external consumer is the surface expansion the startup-scope rule declines. That, and not the file count, is why the changeset is
patchand notminor— an external export is a separate decision with its own changeset.Producer read-only
Nothing on the three producer surfaces changed. The union consumes each producer through its own published predicate —
isSecretHandle(service-settings),collectSecretFieldsandparseSecretRef(objectql),parseCredentialsRef(service-datasource) — so none of the three ref spellings is restated here. A restated prefix would be a second de-facto contract, and the failure it produces is a handle missing from the union.One measured limit, stated rather than papered over:
registerDatasourceDefkeeps onlyschemaModeandexternal.allowWrites, so the engine cannot answer family 3 at all. Persisted artefacts are read fromsys_metadata; code-defined ones must be supplied by the host, and not supplying them is a declared gap rather than a silent zero. Widening the engine to keepcredentialsRefwould be a producer change, so it is not made here.Verification
Union of gates run locally at
929d12c69(the tree this PR pushes), afterpnpm install --frozen-lockfileand a rebuild of the@objectstack/cliclosure:pnpm --filter @objectstack/cli exec vitest run src/utils/secret-reference-union.test.ts— 20 passedpnpm --filter @objectstack/cli typecheck— clean. Both new files are in the tsc program (checked with--listFiles: 1 hit each), so the green is about them.check:nul-bytes,check:objectql-double-limit,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:slot-lookup,check:cross-package-test-inputs(both spellings),check:test-source-alias,check:type-source-resolution,check:published-files,check:page-declaration-shape,check:type-check-coverage,check:changeset-gate-self-tests,check:objectui-changeset,check:durability-log-level,check:startup-registry-verdict,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-ci-filter-parity,check-comment-mask-adoption,check-plugin-teardown-shape, bothdocs-auditchecks,release-rehearsal-clone --self-testpnpm lint— the full repo-wideeslint . --no-inline-config, exit 0. Not a narrowed run, so no narrowing claim is being made.check:i18n,check:i18n-coverage,check:type-check-debtneeded built prerequisites; each was rebuilt and re-run to a real green (check:i18n: 9 packages in sync;check:i18n-coverage: 12 configs, 602 baselined, none new;check:type-check-debt: exit 0 with the closure built)scripts/pm/check-half-states.mjsexits 3 = PREREQUISITE NOT MET (this container has no valid GitHub credential). Reported as NOT MEASURED, not as red.check:objectql-double-limitwas red on the first pass and is the reason the test'sfinddouble now holds the caller's bound by presence, after the filter and before the row copy.Generated by Claude Code