Skip to content

fix(metadata-protocol): key the #3050 authoring gate on authoringChannel so ADR-0090 D11 OWD rules run on host-config deployments - #7710

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-7674-owd-gate-authoring-channel
Aug 11, 2026
Merged

fix(metadata-protocol): key the #3050 authoring gate on authoringChannel so ADR-0090 D11 OWD rules run on host-config deployments#7710
os-zhuang merged 2 commits into
mainfrom
claude/issue-7674-owd-gate-authoring-channel

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #7674

Premise re-verified on current main (6a9dec62c) before implementing

The card's line numbers came from framework 92f26f75. Both halves still held, at slightly different lines:

Two further facts measured while confirming the premise, both of which sharpen the case rather than change it:

  1. packages/lint/src/authoring-rules.ts marks its own validateSecurityPosture rule CLI_ONLY, and its surfaceReason states the grounds verbatim: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on object ... 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.
  2. A repo-wide grep for owd_external_wider found exactly two files: the gate source and its own unit test. object-posture-gate.test.ts was 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:

- if (this.environmentId !== undefined) {
+ if (this.authoringChannel !== 'package-author') {
      await this.runAuthoringGate({ ... });
  }

runAuthoringGate itself 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 reading environmentId, 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 — validateSecurityPosture is CLI_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 real ObjectStackProtocolImplementation on the host-config topology (no environment id, no declared channel, asserted in the harness rather than assumed), the real registerObjectPostureGate wiring security-plugin.ts performs at init, and the real PUT /api/v1/meta/:type/:name route.

  • R2{sharingModel:'private', externalSharingModel:'public_read'} answers 403 owd_external_wider on the active path, the draft path (?mode=draft) and package authoring (?package=) — the issue's three measured 200s. Plus the unset-internal case (an absent sharingModel is private under ADR-0090 D1).
  • R1 — an environment overlay widening a packaged object's internal, and one widening only its external side, both answer 403 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 packaged object is itself scoped to environmentId !== undefined.
  • Every refusal asserts code AND status, 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.
  • Negative direction, treated as load-bearing: four legal OWD pairs still save and store a row; a tightening overlay of a packaged object is still allowed (R1 is directional); and the package-author channel 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.ts carried a case asserting "the #3050 authoring gate keeps its own environmentId !== undefined scope 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 the package-author channel — the one column where both keys agree. It is now a four-cell matrix over (environmentId x authoringChannel), 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-protocol so the test read the reverted dist rather than a stale one:

 x refuses 403 owd_external_wider on 'the active path (bare PUT)'          expected 200 to be 403
 x refuses 403 owd_external_wider on 'the draft path (?mode=draft)'        expected 200 to be 403
 x refuses 403 owd_external_wider on 'package authoring (?package=)'       expected 200 to be 403
 x catches the unset-internal case too                                     expected 200 to be 403
 x refuses an env overlay that widens a packaged object's internal OWD     expected 'NOT_OVERRIDABLE' to be 'owd_widening_forbidden'
 x refuses a widened EXTERNAL side against the packaged baseline           expected 'NOT_OVERRIDABLE' to be 'owd_widening_forbidden'
 Tests  6 failed | 6 passed (12)

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() as NOT_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_WRITABLE is 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

pnpm --filter @objectstack/metadata-protocol test   -> 72 files, 1066 tests passed
pnpm --filter @objectstack/rest test                -> 88 files, 1415 tests passed (incl. the new 12)
pnpm --filter @objectstack/plugin-security test     -> 46 files,  952 tests passed
pnpm --filter @objectstack/objectql test            -> 182 files, 3228 tests passed
pnpm --filter @objectstack/rest typecheck           -> clean

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.json gains @objectstack/plugin-security as 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.json already 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

…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
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 12:32pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 11, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

owd-save-gate: the ADR-0090 D11 authoring gate never runs on any host-config deployment (keyed on environmentId, not authoringChannel)

2 participants