fix(metadata-protocol): key the #3050 authoring gate on authoringChannel so ADR-0090 D11 OWD rules run on host-config deployments - #7710
Conversation
…nel (#7674) The pre-persistence authoring gate call site in `saveMetaItem` was wrapped in `if (this.environmentId !== undefined)`. The CLI's lightweight host-config assembler constructs `new ObjectQLPlugin()` with no options, so `environmentId` stays undefined on a self-hosted app server whose `PUT /api/v1/meta/*` is an END-USER surface — and plugin-security's ADR-0090 D11 object posture gate (R1 `owd_widening_forbidden`, R2 `owd_external_wider`) therefore ran on no host-config deployment at all. This is the proxy-signal hazard #6710 diagnosed and retired for the sibling #4463 gate; the #3050 call site was simply never moved onto the declared `authoringChannel`. Both doors now read one key, and the default stays the gated one. Also adds the integration coverage whose absence let this survive: the gate was 18/18 green in its unit suite while a repo-wide grep for `owd_external_wider` found only the gate source and that suite. The new `packages/rest/src/meta-object-owd-gate.test.ts` drives R1 and R2 through a real sqlite engine, a real protocol on the host-config topology and the real `PUT /api/v1/meta/object/:name` route — draft path, active path and `?package=` — and pins the negative direction too (legal pairs still save; the `package-author` channel still bypasses). Fixes #7674
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Both are ratchets on the new integration test's own surface, not on the fix.
Neither is remedied by raising a number, and neither was raised.
1. `query-options-erasure` — the test surface grew 242 -> 243. The new site was
`engine.find('sys_metadata', { … } as any)` in the suite's persistence probe.
The input is not off-contract, so the remedy is the first one the rule's
message prescribes: drop the assertion and let it infer against
`EngineQueryOptions`, keeping `tsc` as the enforcing channel for those keys
(#4674). No `as unknown as EngineQueryOptions` escape is warranted here, and
the baseline is unchanged — measured back at 242, the ceiling.
2. `TypeScript Type Check` TEST_DEBT — `@objectstack/rest` measured 159 against
a recorded 155, which since #6939 carries no margin. All four were in the new
file: one TS2835 (a relative import without its `.js` extension — the trap
AGENTS.md names, and the class that is already 124 of this entry's debt) and
three TS2554 from hand-rolled `registerObject` calls missing the required
`packageId`. The import gains its extension; the three platform-object
registrations move onto `registerApp` under
`com.objectstack.metadata-objects`, which is the seam
`assembleMetadataProtocol` itself uses — so the harness got more faithful
rather than merely quieter. Re-measured at exactly 155; the ledger is
untouched and no other entry was lowered.
Neither ratchet is evaluated by a package's own `test`/`typecheck` scripts —
rest's tsconfig excludes its own tests while the TEST_DEBT ratchet measures raw
`tsc --noEmit` including them — which is why a green local run said nothing
about either.
Fixes #7674
Premise re-verified on current
main(6a9dec62c) before implementingThe card's line numbers came from framework
92f26f75. Both halves still held, at slightly different lines:if (this.environmentId !== undefined)—packages/metadata-protocol/src/protocol.ts:10058(card said ~L10003).authoringChannelinfrastructure was where the card said: constructor parameter atprotocol.ts:2929(default'environment'),assertRuntimeAuthoringRulesearly-returning for'package-author'atprotocol.ts:3094.Two further facts measured while confirming the premise, both of which sharpen the case rather than change it:
packages/lint/src/authoring-rules.tsmarks its ownvalidateSecurityPostureruleCLI_ONLY, and itssurfaceReasonstates the grounds verbatim: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate onobject... that enforces the same OWD posture rules on every runtime write." That claim was false on every host-config deployment, so the posture was declared-not-enforced on both tables at once.owd_external_widerfound exactly two files: the gate source and its own unit test.object-posture-gate.test.tswas 18/18 green throughout, on a gate nothing ever called.The change
One predicate at the #3050 call site, keyed on the declared channel exactly as #6710 keyed its sibling:
runAuthoringGateitself is untouched, and so is the surrounding save path — the change is additive at the call site, per the serialization constraint on this file. The rest of the protocol diff is comment correction: three docblocks said in prose that the #3050 gate keeps readingenvironmentId, and one of those sentences is exactly how the defect survived #6710 by one gate.One behaviour cell flips beyond the defect, deliberately. A kernel with an environment id and a declared
'package-author'channel was gated before and is not now. That is #6710's direction applied honestly rather than half-applied: a caller that claims to be the package author is treated as one by both doors, and package authoring is gated at build time instead —validateSecurityPostureisCLI_ONLY, and R1's own message prescribes that route ("widen it in the package source and publish through the package pipeline"). No assembly in this repo declares that channel today; only the genuine control plane may.The missing test, which was the other half of the deliverable
New:
packages/rest/src/meta-object-owd-gate.test.ts(12 cases). Nothing hand-built — a real better-sqlite3:memory:engine, a realObjectStackProtocolImplementationon the host-config topology (no environment id, no declared channel, asserted in the harness rather than assumed), the realregisterObjectPostureGatewiringsecurity-plugin.tsperforms at init, and the realPUT /api/v1/meta/:type/:nameroute.{sharingModel:'private', externalSharingModel:'public_read'}answers 403owd_external_wideron the active path, the draft path (?mode=draft) and package authoring (?package=) — the issue's three measured 200s. Plus the unset-internal case (an absentsharingModelisprivateunder ADR-0090 D1).owd_widening_forbidden. R1 belongs here and not only in the unit suite because on a host config it is the only guard: the ADR-0005 two-tier authorization that would normally refuse an overlay of a packagedobjectis itself scoped toenvironmentId !== undefined.codeANDstatus, and also that nothing was persisted — a 403 answered after the row landed would still be the defect, and a status-only assertion cannot see it.package-authorchannel still bypasses the gate, so the #4463 运行时发布门在 host-config 拓扑(environmentId 未绑定)上整体不跑——#5086 只把 code-only 拒绝移出了同一条短路 #6710 carve-out is intact. That last case is green before and after on purpose — it pins the thing that must not move.Also replaced, not re-spelled:
protocol.runtime-authoring-gate.test.tscarried a case asserting "the #3050 authoring gate keeps its ownenvironmentId !== undefinedscope check, and it must stay keyed there". That sentence was the defect written down as a pin, and it could not see the hole because it only drove the control-plane row through thepackage-authorchannel — the one column where both keys agree. It is now a four-cell matrix over (environmentIdxauthoringChannel), which is what makes the two keys distinguishable, plus a case showing both gates live on the topology that had neither.Reverse verification — direction predicted before running
Prediction: reverting the one predicate should turn every refusal case red by answering 200 (not by throwing), and leave every negative case green. Measured, after rebuilding
metadata-protocolso the test read the reverteddistrather than a stale one:The four R2 cases reproduce the issue's measured 200s in process. The two R1 cases fail differently and informatively: with the gate dead the write travels one door further and is refused by
SysMetadataRepository.assertAllowed()asNOT_OVERRIDABLE— a different code from a different layer, which is exactly why a throw-only or "did it fail" assertion would have proven nothing here. All six negative cases stayed green, so the fix adds enforcement without refusing legal authoring.That run also surfaced a latent order-dependency in the new suite and it is fixed:
OS_METADATA_WRITABLEis memoised in two places, and with the fix in place the posture gate refuses first so the repository's memo is never warmed — clearing only the protocol's looks sufficient until the fix comes out. The harness now clears both.Verification
Gate families touching this surface, all green locally:
check:nul-bytes,check:error-code-casing,check:route-envelope,check:durability-log-level,check:startup-registry-verdict,check:meta-type-normalized,check:published-files,check:engine-double-contract,check:adr-anchors,check:adr-links,check:type-check-coverage,check:empty-changeset,check:adr-0087-registration. The consumer sweep direction used for the build closure was the suffix form ('@objectstack/rest^...', i.e. rest's dependencies) — this change narrows no exported type, so there is no downstream signature surface to sweep.packages/rest/package.jsongains@objectstack/plugin-securityas a devDependency so the suite can register the real gate rather than a copy of it. Checked for a cycle first: plugin-security's transitive dependency closure (deps + devDeps, 17 packages) does not reach@objectstack/rest.Scope
Filed and fixed as an authoring-validation gap, not a live disclosure — external-principal enforcement is #2696-planned, so a wider external baseline discloses nothing today. Nothing here touches external-principal read enforcement.
Not done, and flagged rather than done quietly:
scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.jsonalready exists for this file and anchors ADR-0029 / ADR-0119. Extending it with the ADR-0090 D11 activation would be a reasonable follow-up, but it is a shared satellite of the hottest file in the repo and outside this card's stated surface; the code comment carries the ADR and issue references in the meantime.Generated by Claude Code