Skip to content

fix(metadata,spec): publish 门兜底 —— publishPackage 接门 + 匹配器装载期拒绝(#5040 E7b) - #5203

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5189-publish-gate-backstop
Aug 4, 2026
Merged

fix(metadata,spec): publish 门兜底 —— publishPackage 接门 + 匹配器装载期拒绝(#5040 E7b)#5203
os-zhuang merged 2 commits into
mainfrom
claude/issue-5189-publish-gate-backstop

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #5189
Part-of #5040(E7b)。前置 PR #5188(d21c001)已落 main,门函数在。

事实

E7(#5111)把五道逐端点门挂在 ObjectStackDefinitionSchema 上,覆盖了所有解析 stack 的入径。#5189 证实:一条存量 api 条目根本不必属于任何 stack —— MetadataManager.publishPackage、直接 metadata.register()、Studio 元数据写接口,三条路各自逐条造条目,一道门都没经过

绕过后的后果按门分化:

  • 不支持子集 / 映射 / 命名空间三类门有安全的降级:执行链答结构化 501 并点名条目;落在 apps/{namespace}/ carve-out 之外的路径干脆匹配不上;
  • ADR-0121 D6 没有任何运行时对偶:运行时忠实执行 authRequired: false,而 deriveBucketConfigenabledtrue 的预算返回 null、不计量。于是绕过铸出一个匿名可达、零配额的执行入口 —— 正是 D6 要防的形状。

处置:两道门,一个判据

采用 issue 建议的 A + C,关键在于两层跑的是同一个门函数,不产生第二套判据。

1. Publish 层 —— publishPackage 对包内 api 条目跑 validateApiEndpointDeclarations,失败即在它既有的 validationErrors 出口上让整次发布失败(发布本就是原子的),逐条点名端点与待修的 key。

这一遍不受 options.validate 管辖:其余检查都是尽力而为的质量检查,关掉是便利;这道门决定的是「一个对外可达、可能匿名的执行入口是否诞生」,而 D6 没有运行时对偶接得住漏网的。给安全门加开关,就是本单要关掉的那个绕过。

不满足 ApiEndpointSchema 的条目也在此失败 —— 这不是额外严格,而是前置条件:没解析出来的形状无从过门,而且它在装载期也会被匹配器的响亮跳过拒之门外。

2. Load 层(纵深防御) —— buildEndpointIndex 对每条已解析条目再跑一遍免身份子集(不支持子集 / 映射 / policy 含 D6),经新增的 identityFreeEndpointGateFailure 委托给全量门同一个 firstFailure。未过门的条目被排除出索引,并以与解析失败跳过相同的 error 级别点名。绕过因此从「匿名零配额可执行」变成「404 + 一条响亮日志」—— 安全的失败姿态。

刻意不对称,值得写明:命名空间门与唯一性门不在装载期施加。前者需要 manifest.namespace,而一条存量行不携带 manifest —— 从「正被审判的那条 path」反推命名空间是循环论证;后者是 per-stack 规则,而下方既有的重复认领裁决已在全库范围覆盖同一问题。

身份:publishPackage 知道命名空间吗?

不知道。 MetadataManager 只按 packageId 索引条目,不存在 manifest 概念(全文件无 manifest 一词)。

因此新增加性选项 publishPackage(id, { namespace }):持有 manifest 的调用方传入显式 manifest.namespace,即可跑全量门;不传则命名空间门失败,该包的 api 条目发不出去 —— 这是规则本身,不是待绕开的限制(一次无法证明命名空间的发布,不该在某个命名空间下铸出 URL)。

不会从被审判的条目里反推命名空间:条目上的 namespace 是作者填的数据,不是身份,采信它会让 D1/D2 的 carve-out 门形同虚设(作者只需把命名空间填成自己 path 已经用的那个)。不含 api 条目的包完全不受影响(该遍直接早返回)。

契约面

packages/spec/src/api/index.ts 导出门模块的四个公共名(#5111 时它是包内私有,因为唯一消费者就在隔壁文件;#5189 证明 stack schema 不是唯一的门)。api-surface.json 恰好只在 ./api 上多这四项:

+ EndpointGateIdentity (interface)
+ EndpointGateIssue (interface)
+ identityFreeEndpointGateFailure (function)
+ validateApiEndpointDeclarations (function)

门模块自身的判定逻辑一字未改,只加了一个委托给同一 firstFailure 的加性导出。

测试夹具的一处修正

endpoint-matcher.test.ts / metadata-manager-match-endpoint.test.ts 的基础夹具补上了 E7 target 门要求的 objectParams。没有它,那个夹具描述的是一条运行时会答 501 的端点 —— 装载期门(正确地)把它排除,于是它作为夹具也就没了意义。同理,preserves an explicit authRequired: false 一例补上了已装配的预算:D6 之后,那是「authRequired: false 能原样往返」仍然可观测的唯一形状。

验证

pnpm --filter @objectstack/spec test        → Test Files 305 passed | Tests 7809 passed
pnpm --filter @objectstack/metadata test    → Test Files  18 passed | Tests  407 passed
pnpm --filter @objectstack/spec typecheck   → Done(0 error)
pnpm --filter @objectstack/spec check:generated → ✓ All 8 generated artifacts are up to date
pnpm --filter @objectstack/spec check:exported-any        → ✅ 1843 types + 1594 schemas,无 any
pnpm --filter @objectstack/spec check:dual-source-exports → ✅ 4248 names,0 dual-source
pnpm check:adr-anchors / check:durability-log-level / check:startup-registry-verdict → 全 OK
pnpm check:type-check-coverage → OK(62/77 covered;metadata 仍在 DEBT 账,新增文件 0 error)
eslint(全部改动文件)→ 无输出

@objectstack/metadatatsc --noEmit 原始报错全部落在既有测试文件的 TS2835(无扩展名相对导入,既有债),本 PR 新增/改动的源文件与新测试文件 0 error

新增测试:

  • publish 层(packages/metadata/src/publish-endpoint-gate.test.ts,15 例)—— D6 无限流被拒并点名端点+key;rateLimit 存在但未装配(enabled 默认 false)被拒;装配后放行;validate: false 关不掉;其余四门同行;命名空间缺失时只报一次并带本路径自己的补救;不从条目反推命名空间;发布信封 metadata 与裸文档两种存储形状都读得对;无 api 条目的包零影响;一条坏端点让整包发布失败、好条目仍是 draft。
  • load 层(endpoint-matcher.test.ts +8 例,metadata-manager-match-endpoint.test.ts +2 例)—— D6 违规条目被排除、日志响亮、匹配器 miss;装配预算后可服务;其余免身份门同样排除;命名空间门施加(并写明为何);未过门条目从合法的重复认领者手里抢走路由;既有的响亮跳过与重复认领测试全绿。
  • spec 层(apis-publish-gates.test.ts +6 例)—— 免身份子集与全量门的一致性,以及两处不对称(命名空间、唯一性)的正反两面。

变更集

.changeset/endpoint-publish-gate-backstop.md —— @objectstack/spec minor(新导出)+ @objectstack/metadata minor(新 namespace 选项 + 发布/装载行为收紧)。未触碰 content/docs/releases/

越范围发现(已另行归档,不在本 PR 内修)

#5206 —— api 根本不在 DEFAULT_METADATA_TYPE_REGISTRY / BUILTIN_METADATA_TYPE_SCHEMAS 里,所以 metadata-protocolsaveMetaItemapi 条目一道校验都不跑(比 #5189 正文推断的「只按 ApiEndpointSchema 校验」还弱一层);同时 publishPackageDrafts(Studio「全部发布」的真实入径)也没有第一道门。本 PR 的第二层已经兜住了它的安全后果(未过门条目一律不进索引),因此 #5206 降级为「拒绝得晚、而非拒绝不了」+「活类型没有契约」,不再是安全洞。文件面在 packages/metadata-protocol + packages/spec/src/kernel,与本单互斥。


🤖 Generated with Claude Code

https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd

#5040 E7b)

E7 (#5111) hung the five per-endpoint `apis:` gates on
`ObjectStackDefinitionSchema`, covering every path that parses a STACK. #5189
proved a stored `api` item need never have been part of one:
`MetadataManager.publishPackage`, a direct `metadata.register()` and a Studio
metadata write each mint one item at a time and saw no gate at all.

Three of the five gates degrade safely when bypassed — the executor answers a
structured 501 naming the item, and a path outside the `apps/<namespace>/`
carve-out matches nothing. ADR-0121 D6 has NO runtime counterpart: the runtime
honours `authRequired: false` faithfully and `deriveBucketConfig` returns null
for a budget whose `enabled` is not `true`, so the bypass minted an anonymous,
zero-quota execution entry point — the exact shape D6 forbids.

Two doors, one judge:

- publish — `publishPackage` runs `validateApiEndpointDeclarations` over the
  package's `api` items and fails the publish on its existing
  `validationErrors` surface, naming each endpoint and the key to fix. NOT
  governed by `options.validate`: an opt-out on a security gate is the bypass
  this closes. An item that does not satisfy `ApiEndpointSchema` fails here too
  — an unparsed shape cannot be gated, and the matcher refuses it at load
  anyway.
- load — `buildEndpointIndex` re-applies the identity-free subset (supported
  subset, mapping, policy/D6) to every parsed item via the new
  `identityFreeEndpointGateFailure`, which delegates to the SAME `firstFailure`
  the full gate runs. A declaration that never passed publish is EXCLUDED and
  named at `error` level, so the bypass answers 404 with a loud log instead of
  answering anonymously and unmetered. The namespace and uniqueness gates are
  deliberately not applied there: both need a stack identity a stored row does
  not carry, and deriving a namespace from the very path being judged would be
  circular.

Identity at publish: `MetadataManager` indexes items by `packageId` and carries
no manifest, so it cannot prove a namespace. New additive option
`publishPackage(id, { namespace })` lets a caller holding the manifest supply
the explicit `manifest.namespace`; without it the namespace gate fails and the
package's `api` items do not publish — the rule, not a limitation. It will NOT
infer one from the items being judged: an author-supplied value would make the
D1/D2 carve-out gate vacuous. Packages declaring no `api` items are untouched.

`packages/spec/src/api/index.ts` now exports the gate module's four public
names (it was package-internal in #5111, whose only consumer was one file
away); `api-surface.json` moves by exactly those four on `./api`.

Test fixtures in `endpoint-matcher.test.ts` /
`metadata-manager-match-endpoint.test.ts` gained the `objectParams` E7's target
gate requires — without it they describe an endpoint the runtime would 501 on,
which the load gate now (correctly) excludes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
@vercel

vercel Bot commented Aug 4, 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 4, 2026 9:15am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l and removed documentation Improvements or additions to documentation tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via packages/metadata, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 09:10
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 09:10
@os-zhuang
os-zhuang marked this pull request as draft August 4, 2026 09:12
auto-merge was automatically disabled August 4, 2026 09:12

Pull request was converted to draft

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 4, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 09:17
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 09:17
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit c52e608 Aug 4, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5189-publish-gate-backstop branch August 4, 2026 09:38
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…-ai#5040 E8) (objectstack-ai#5230)

* test(e2e,showcase): 执行器收官验收 —— showcase 端点回迁 + 真实 boot e2e (objectstack-ai#5040 E8)

objectstack-ai#4936 把 showcase 的两条 `apis:` 声明注释掉,理由是当时端点面全链路零执行:没有
挂载、没有匹配器、每个键(包括 `authRequired`)解析通过而不生效。E1–E7 建成执行器
并把整面硬拒收窄为逐端点 publish 门之后,那条理由不复存在。本单把两条声明按原意
恢复 —— 同名、同 target、同 `authRequired`、同 `cacheTtl` —— 只做 ADR-0121 D1
要求的一处修改:路径迁入本应用的命名空间保留区 `/api/v1/apps/showcase/…`。

- examples/app-showcase:恢复 TaskFeedEndpoint / InquiryPurgeEndpoint;coverage
  的 `apis` 从 waived 翻回 demonstrated(理由改写为「由真实 boot 测量」而不是
  「声明即证明」—— 这正是 objectstack-ai#4936 抓到的那类假覆盖);gap-fill 的反向断言改回正向,
  并新增命名空间与「零匿名面」两条静态钉子。
- packages/qa/dogfood:两个真实 boot e2e。showcase 一份走真实 artifact 摄入路径
  (MetadataPlugin + local-file),证明匹配命中执行、find 的 data 与内建 /data
  路由逐字节相同、匿名 401、cacheTtl 只随成功答案、挂载点下未声明路径与挂载点外
  的裸 404 完全一致、/meta/api 与 /openapi.json 描述的正是挂载的东西;fixture
  一份补上 ADR-0121 D6 的匿名分支 —— 省略 authRequired 拒绝匿名、显式 false 服务
  匿名、已装配预算耗尽后 429 且 Retry-After 真的在线上。

RED-first:两份断言先于 showcase 回迁写就并运行,pre-restore 状态下 9 条失败(全部
是 objectstack-ai#4936 记录的那个裸 404),回迁后 14 + 7 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd

* test(e2e): 直写拒绝探针接上 E7b,fixture 用 ApiEndpointInput 表达「省略 authRequired」

E7b(objectstack-ai#5189/objectstack-ai#5203)在 main 上落地之后,「一次绕过 publish 的直写不会变成活路由」
从待办 TODO 变成可以真实断言的事:往 store 里直接写一条 `authRequired: false`
且没有已装配预算的 api 条目(D6 最危险的那个形状),两种调用者都必须拿到 404 ——
不是「因为匿名所以 401」,而是压根没有这条路由。

同时把 fixture 里那条「故意省略 authRequired」的声明改标 `ApiEndpointInput`:
`ApiEndpoint` 是 schema 的输出类型,`.default(true)` 已被物化,该键在上面是必填,
所以用它做注解的 TS 作者根本写不出升级指南推荐的那个安全形状(另立 objectstack-ai#5227 记录)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MetadataManager.publishPackage 不经 E7 端点门 —— ADR-0121 D6「匿名须限流」在 Studio 发布路径上不成立

2 participants