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

@claudeclaudeBot 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'){awaitthis.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

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/lteststooling

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

@os-zhuang@claude