fix(metadata-protocol,objectql): activate the #4463 runtime authoring gate from a declared authoring channel instead of the environmentId proxy (#6710) - #6971
Merged
Conversation
…ntId (#6710) #4463 的运行时发布门(26 条共享 AUTHORING_RULES)此前挂在 `if (this.environmentId === undefined) return;` 后面。这条短路本意是 ADR-0005 「包作者自有 bootstrap 通道」的 carve-out——carve-out 本身正确并保留——但 `environmentId` 是一个行作用域键,而两种意图相反的拓扑都让它为 undefined: 真正的控制面,以及 CLI 的轻量 host-config 装配(`serve.ts` 的 `config.objects && !hasObjectQL` 分支,`new ObjectQLPlugin()` 无参)。后者是任何 带实例化插件的 `objectstack.config.ts` 的形状(`isHostConfig` → `shouldBootWithLibrary === false`),旗舰 showcase 即在其上,而它的 `PUT /api/v1/meta/*` 是终端用户面——于是自托管应用服务器上 26 条规则一条都不跑。 本轮在基线 68feaad 上做了 boot 级复测:environmentId 为 undefined,#4463 自己 那条 broken-CEL 审批流直接越过门进入持久化。 改法:通道由装配显式声明,门按声明激活。新增公开插件选项 `authoringChannel: 'environment' | 'package-author'`(ObjectQLPlugin 与 createMetadataProtocolPlugin 各一份),经 `assembleMetadataProtocol` 这一两种挂载 共享的唯一接缝下沉到协议实例。缺省(不写)即 `'environment'`,即开门设防: 忘记声明得到的是更多而非更少的强制——这正是本单要设计掉的失败模式。选项刻意是 通道名而非布尔:`skipAuthoringRules: true` 是同样的字节、相反的语义。 `environmentId` 保留其余全部职责(行标记与过滤、ADR-0005 overlay 白名单、#3050 authoring gate 的作用域、本地元数据表 provisioning),本单只搬这一处激活判断。 cloud 侧后续(不在本仓):`control-plane-preset.ts` 需声明 `authoringChannel: 'package-author'`;在此之前控制面按安全方向受门约束,由既有 `OS_ALLOW_UNLINTED_METADATA_WRITES` 逃生舱缓冲。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Contributor
Author
|
ACCEPT — PM step-7 review ( Verified against GitHub, not against the report:
Two calls the dev flagged, both endorsed:
Generated by Claude Code |
os-zhuang
marked this pull request as ready for review
August 9, 2026 07:05
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6710
Implements the maintainer's A1 ruling of 2026-08-09 05:41Z (comment 5229995235): the gate is active by default on every kernel, and the ADR-0005 "package author's own bootstrap channel" carve-out becomes an explicit declaration plumbed through
assembleMetadataProtocol.The defect
ObjectStackProtocolImplementation.assertRuntimeAuthoringRules— the #4463 runtime publish gate that runs the 26 sharedAUTHORING_RULES(the same tableos validate/os build/os lintrun) — opened with:That short-circuit was meant to express ADR-0005's carve-out, and the carve-out itself is legitimate and stays: a control-plane kernel installing a package is not an author publishing into a live tenant. The key was the problem.
environmentIdis a row-scoping key, and two topologies with opposite intents both leave it undefined:environmentIdassembleMetadataProtocolassembleMetadataProtocolPUT /api/v1/meta/*The second is
serve.ts'sconfig.objects && !hasObjectQLauto-register branch (new ObjectQLPlugin()with no options), which is the shape anyobjectstack.config.tswith instantiated plugins gets (isHostConfigyieldsshouldBootWithLibrary === false) — including the flagship showcase. So a self-hosted app server ran zero of the 26 rules on every publish. For a Studio tenant or an MCP/AI author this gate is not the weakest of four doors, it is the only one: asys_metadataoverlay row is never in the CLI's config file, so there is noos lintfor it.#5086 had already retired this same proxy for the code-only refusal a few hundred lines below, for the same stated reason — it just moved that one site.
Premise, re-measured on this base
Re-verified at boot level on
origin/main@68feaadd6(not a source read): a kernel built exactly asserve.ts:1000builds it, then #4463's own measured body (the broken-CEL approval flowrecord.owner ==) pushed throughsaveMetaItem:The throw comes from the engine's persistence layer, not the gate — execution had already run past
assertRuntimeAuthoringRules(which throws 422INVALID_METADATAbefore any engine call). The producer is unchanged on this base:packages/cli/src/commands/serve.ts:996-1001still constructsnew ObjectQLPlugin()with no options. Premise holds.The shape implemented, and why it is fail-safe
A new public plugin option states what a kernel is; gate activation reads that instead of row scope.
New public contract shape (explicitly declared, as the ruling requires):
Exported from
@objectstack/metadata-protocol: the typeMetadataAuthoringChannel, the newAssembleMetadataProtocolOptions, and the widenedMetadataProtocolPluginOptions/ObjectQLPluginOptions. All four surfaces are additive and optional — no existing call site changes shape.Three properties are deliberate:
'environment', resolved at the assembly seam (options.authoringChannel ?? 'environment') as well as at the constructor, so every path that predates this option — includingObjectQLPlugin's legacy positional(ObjectQL, hostContext)constructor, which returns before any option is read — lands on the gated channel. The failure mode being designed out is precisely "a future assembly variant nobody thought about", which is how the host-config topology got here.skipAuthoringRules: truewould be the same bytes with the opposite meaning — a switch any assembly could reach for to make a red publish go away. A caller has to claim to be the package author to be treated as one. There is deliberately no env-var fallback (unlikeskipSchemaSync): a deployment must not be able to turn an end-user guardrail off from outside the code. The per-write hatch that does exist isOS_ALLOW_UNLINTED_METADATA_WRITES([runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 D4), which degrades a refusal to a loud log rather than silencing it.assembleMetadataProtocolis what both the built-in mount and the delegated ADR-0076 Step 2 mount call, so enforcement cannot silently differ by mount style. Pinned in both directions.environmentIdkeeps every other job: theenvironment_idstamp and filter, the ADR-0005 overlay whitelist, the #3050 authoring gate's own scope check, and the local metadata-storage provisioning decision. Only this one activation moved — pinned by a dedicated case asserting the #3050 gate still turns on and off byenvironmentIdwhile the #4463 gate follows the declaration.File surface (no deviation from the dispatch)
packages/metadata-protocol/src/protocol.tsMetadataAuthoringChanneltype;authoringChannelfield + 4th constructor param defaulting to'environment'; the activation line itselfpackages/metadata-protocol/src/plugin.tsMetadataProtocolPluginOptions.authoringChannel; newAssembleMetadataProtocolOptions; threads it into the protocol constructionpackages/metadata-protocol/src/index.tspackages/objectql/src/plugin.tsObjectQLPluginOptions.authoringChannel; field; forwards it to the assemblypackages/metadata-protocol/src/protocol.runtime-authoring-gate.test.tspackages/metadata-protocol/src/protocol.platform-schedule-org-gate.test.tspackages/objectql/src/plugin.authoring-channel.test.ts.changeset/authoring-channel-declaration.mdThe
saveMetaItemtwo-tier write-gate region (#6190, in flight) was not touched.packages/specwas not needed. Nocontent/docs/releases/edit.The one fixture the sweep found outside the edited test file:
protocol.platform-schedule-org-gate.test.ts(#6285) carried its own copy of the carve-out pin, spelled with the retired key. Disposition re-spell, not delete: the case's subject (the carve-out) is intact, only its expression changed, so it now constructs with'package-author'. It also gained a companion case, because #6285's guardrail is one of the 26 and its reach widened too — without it the file would assert only the side that stayed the same.Reverse verification
Predictions were written down before running (
/tmp/.../issue-6710/reverse-predictions.md), then the activation line alone was reverted toif (this.environmentId === undefined) return;with the plumbing and tests left in place.Predicted 7 red, measured 8. One miss, recorded rather than tidied away.
protocol.runtime-authoring-gate.test.ts— predicted 4 red, measured 5 (11 pre-existing #4463 cases stayed green throughout):environment⇒ GATEDpackage-author⇒ bypassedenv_test, channel omitted ⇒ GATEDenv_test, channelenvironment⇒ GATEDenv_test, channelpackage-author⇒ bypassedTwo of these are worth stating plainly rather than as a pass count:
envId undefined + channel package-author ⇒ bypassedis green before and after, because the old code bypasses there too — for the wrong reason. It is honest coverage of the surviving carve-out, but it is not evidence for this change, and the matrix says so in its own comment.envId env_test + channel package-authorgoes red on the revert because the OLD code gates where the new code bypasses. That is the row proving the declaration is genuinely read rather than ignored.does not disturb the OTHER gateswent red. Cause, on inspection: its tenant leg saves a broken flow, and under the old keying the [runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 gate refuses that write with a 422 before the OWD posture is lint-only at runtime: env can widen a packaged object's sharingModel via OS_METADATA_WRITABLE, and external≤internal (ADR-0090 D11) is never enforced on the write path #3050 gate is ever reached, so the observed gate list stayed empty. The red is real and informative (it pins the ordering of the two gates), but I predicted green because I only reasoned about the OWD posture is lint-only at runtime: env can widen a packaged object's sharingModel via OS_METADATA_WRITABLE, and external≤internal (ADR-0090 D11) is never enforced on the write path #3050 gate's own condition and not about what precedes it.plugin.authoring-channel.test.ts— predicted 3 red, measured 3 (exactly cases 1, 3, 5).The most load-bearing detail: the boot-level case failed on
expected undefined to be 'INVALID_METADATA', not on "did it throw". The unfixed build throws too — the engine's bareNo driver available, anErrorwhosecodeandstatusare both undefined. Arejects.toThrow()assertion would therefore have stayed green on the exact topology this issue is about. Every rejection-class case here asserts the ADR-0112code+statuspair for that reason.Side observation from the revert:
pnpm --filter @objectstack/metadata-protocol buildfails its DTS step under the reverted line, becauseauthoringChannelbecomes a field nothing reads. The ESM/CJS bundles are emitted before DTS fails, which is why the behavioural measurement above is still valid.Tests
All run through
flock /tmp/os-heavy-verify.lockwithNODE_OPTIONS=--max-old-space-size=4096.rest/runtime/cliare in the sweep because this change widens enforcement: they boot kernels that were previously on the bypassed side. All green with no fixture edits needed.Family gates run locally (they live inside the ESLint CI job, not in
pnpm test):check:engine-double-contract,check:driver-memory-census,check:error-code-casing,check:route-envelope,check:meta-type-normalized,check:init-service-contract,check:published-files,check:doc-authoring,check:docs-audit-scope,check:nul-bytes— all PASS.Follow-up in
cloud(NOT in this PR)One line, in
cloud'spackages/.../control-plane-preset.tsaround:99— the only genuine consumer of this carve-out in either repo:The cross-repo window is accepted by the ruling and is safe by construction: until that lands, the control plane runs gated — the safe direction — softened by the existing
OS_ALLOW_UNLINTED_METADATA_WRITEShatch. Same coordination shape as ADR-0076 Step 2 (#2462).Generated by Claude Code