Skip to content

finding: POST /packages/:id/publish-drafts 整包 draft 批量转正,零能力的已认证调用方即可调用(实测 200 vs 同族 _migrate-stored 403) #7023

Description

@os-project-manager

在 #6599 的消费方普查里撞到的(那张卡问的是「谁在调 /meta/_drafts」;顺着 usePublishAllDrafts 的 Publish 动作走到了这条写面)。未修,按 PD #10 立卡。

实测:一个零能力的已认证调用方能做什么

不是「没看到能力门禁」这种静态观察 —— 直接驱动 dispatcher 量的。调用方上下文 { userId: 'u_portal', systemPermissions: [] },一个会话、一条能力都没有:

### zero-capability caller context ### {"userId":"u_portal","systemPermissions":[]}
### HTTP status ### 200
### publishPackageDrafts invoked? ### 1
### drafts promoted to ACTIVE ### ["app.hr:account","app.hr:salary_review"]
### body ### {"success":true,"data":{"success":true,"publishedCount":2,"failedCount":0,
              "published":[{"type":"object","name":"account"},
                           {"type":"object","name":"salary_review"}],"failed":[]}}

同一个调用方,打隔壁那条同族路由 POST /metadata/_migrate-stored:

### _migrate-stored status for the SAME caller ### 403
### migrateStoredMetadata invoked? ### 0

同一个调用方、同一次运行、同一类操作 —— 一条 200 并真的把整包 draft 转正,一条 403 且被调函数一次都没进。

代码面

POST /packages/:id/publish-drafts → packages/runtime/src/domains/packages.ts:160。

整个 packages.ts(739 行)里 systemPermissions / isSystem / manage_metadata / FORBIDDEN / 403 的命中数是 0。挂载侧 mountPackagesRoute(packages/runtime/src/dispatcher-plugin.ts:984)也只是 dispatcher.dispatch(...) 的转发,没有包任何门禁。所以这条路由之上除全局 requireAuth 外没有任何判据。

对照物就在同一个 dispatcher 里:_migrate-stored(domains/meta.ts)显式判 ec?.isSystem || systemPermissions.has('manage_metadata'),并且在解析 protocol 之前判,理由写在注释里(不让调用方拿 501/200 的差别去探测)。

这条路由做的事

publishPackageDrafts 把该 package 名下每一条 pending DRAFT 行提升为 active;seed 类型的 draft 被发布即意味着装载数据行(路由里 applyPublishedSeeds 那段);随后还会清掉 ADR-0045 的发布门 —— #4829 / PR #6942 刚把它挪到机器管理键 app._unpublished,由 publish-drafts 置 false。也就是说这一次调用同时是「schema 生效」「数据落库」「半成品应用对真实用户可见」。ADR-0045 自己写过这个门的失败方向判断:门失败开放会把半成品应用静默暴露给真实用户,比过度限制严格更糟(docs/adr/0045-...md:192)。

为什么它值得单独一张卡

它和 #6603 是同一形状 —— 而 #6603 刚被维护者裁为需要 manage_metadata,判词是「能写 schema 的人就该是能看见完整 schema 的人」(评论 5225531464)。#6603 管的是单条 PUT /meta/:type/:name;这条是整包批量转正,按同一条判据赌注更大,却是两条里没有门禁的那条。

同族参照:#6920(匿名 mass assignment)、#6599(_drafts 读面,其所述字段级泄露已实测证伪,见 PR #7014)。

不在本卡里替维护者选修法

至少有「照 #6603 判 manage_metadata」和「按 package 所有权/作者身份判」两种读法,成本与语义不同;而且 /packages 域下同样裸奔的还有 discard-drafts / revert / rollback / enable / disable,要不要一起收口是域级决定,不是这条路由的局部决定。留给分诊与维护者。

复现

驱动 HttpDispatcher.handlePackages('/app.hr/publish-drafts', 'POST', {}, {}, { request: {}, executionContext: { userId: 'u_portal', systemPermissions: [] } }),protocol 侧放一个 publishPackageDrafts 双档记录被提升的条目;同一上下文再驱动 handleMetadata('_migrate-stored', ctx, 'POST', {}, {}) 作对照。上面两段输出即为该探针的原始 stdout。

立卡时的查重说明

开卡前的 issue 关键词查重被账号级 REST 速率限制挡住(本班次共享身份,多次退避重试仍 API rate limit already exceeded)。改用本地信号做的查重:git log -i --grep=publish-drafts 与全仓 publish-drafts + 权限关键词交叉搜索,命中的都是发布语义(ADR-0045 可见性、seed 装载、org 作用域、端点发布门),没有一条关于这条路由的授权。若分诊发现已有同族卡,按重复合并即可。

Activity

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    只读普查回报(不落门、无 PR)

    基线 origin/main = c3f491626。objectui HEAD = 0cf8f0f。
    下面每条都标了 实测 / 静态阅读,两者不混写。


    0. 先复现:卡片断言成立,而且比卡片更强

    实测。 临时 vitest 探针驱动 HttpDispatcher(与仓内既有能力门测试 packages/runtime/src/domains/meta-migrate-stored.test.ts 同一驱动法),protocol 侧全是 vi.fn() 双档并记录调用顺序。原始 stdout:

    ##### P1 直接驱动 handler,caller = {userId:'u_portal', systemPermissions:[]} #####
    ### publish-drafts HTTP status ### 200
    ### publishPackageDrafts invoked? ### 1
    ### call trace ### ["publishPackageDrafts(app.hr)","getMetaItem(seed/demo_rows)",
                        "getMetaItems(type=app)","saveMetaItem(app/hr_console _unpublished=false)"]
    ### _migrate-stored HTTP status (SAME caller) ### 403
    ### migrateStoredMetadata invoked? ### 0
    ### body ### {"success":false,"error":{"code":"PERMISSION_DENIED",
                  "message":"Rewriting stored metadata requires the `manage_metadata` capability.",
                  "httpStatus":403}}
    

    卡片说的「同一调用方,一条 200 一条 403」复现成功。

    但还有一档更低的,卡片没量到:

    ##### P2 走完整 dispatch() 管线,context = { request: {} }(不带任何凭据) #####
    ### resolved executionContext ### {"positions":["guest"],"permissions":[],"systemPermissions":[],
                                       "isSystem":false,"principalKind":"guest",
                                       "org_user_ids":[],"accessible_org_ids":[]}
    ### publish-drafts HTTP status ### 200
    ### publishPackageDrafts invoked? ### 1
    ### _migrate-stored HTTP status (anonymous) ### 401
    ### migrateStoredMetadata invoked? ### 0
    

    实测: 走完整 dispatch()(resolveRequestScope → 身份解析 → enforceAuthGate → enforceProjectMembership → 域注册表),一个连 userId 都没有、身份解析产出 principalKind: 'guest' 的调用方,打 publish-drafts 仍得 200 并真的调进 publishPackageDrafts;同一管线上 _migrate-stored 得 401。

    ⚠️ 这一档的边界要说清楚:P2 是 dispatcher 层的测量。真实部署(plugin-auth + transport 中间件全装)下匿名请求会不会在到达 dispatcher 之前被别的中间件先拦掉 —— 未实测。静态阅读的旁证是:mountPackagesRoute(packages/runtime/src/dispatcher-plugin.ts:984-1001)直接 server.post 挂 dispatcher.dispatch,不经过 mountRouteOnServer 那道 401 门(dispatcher-plugin.ts:180-223,那道门只服务插件发出的 RouteDefinition,判据是 route.auth !== false && !user)。

    探针已删除、未提交、无分支推送。


    1. 今天谁在调这条路由

    objectstack(搜索面:全仓 ts/tsx/mts/mjs/js/json/yaml/mdx/md,关键词 publish-drafts / publishDrafts / publishPackageDrafts,排除 node_modules、dist、CHANGELOG)

    静态阅读。 生产调用方只有 1 个:

    • packages/client/src/index.ts:1270 —— SDK packages.publishDrafts(id, opts?),POST {baseUrl}{route}/{id}/publish-drafts。

    而这个 SDK 方法在两仓内都没有任何生产消费方:objectstack 内唯一引用是 packages/client/src/packages-lifecycle.test.ts:42(路由绑定测试);objectui 全仓搜 packages.publishDrafts / .publishDrafts( 零命中。

    搜了但没命中的地方(也是数据):

    • packages/cli/src —— publish-drafts / publishDrafts 零命中。os 没有任何命令打这条路由。
    • examples/(app-showcase、app-todo 等)—— 零命中。
    • packages/qa/(dogfood 全部 test)—— 零命中。docs/qa/platform-checklist/areas/studio-authoring.json:310/344/346 把它写成人工步骤,该条目 automated.ref 指向的 packages/qa/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts 自己不打这条路由。
    • 其余命中全部是 runtime 自己的路由测试、metadata-protocol/objectql 的协议层测试、ADR/文档、changeset。

    objectui(HEAD 0cf8f0f)

    静态阅读。 生产调用方 4 处,全在 packages/app-shell,全是裸 fetch('/api/v1/packages/:id/publish-drafts'),都不走 SDK:

    1. packages/app-shell/src/views/metadata-admin/PackagesPage.tsx:359 —— Studio 的 Packages 详情页 Publish 按钮。
    2. packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:561 —— Studio 设计面。
    3. packages/app-shell/src/preview/usePublishAllDrafts.ts:76 —— 共享 hook,两个宿主:console/home/HomePage.tsx:134(Home 的 pending-drafts 横幅)与 preview/DraftPreviewBar.tsx:31(ADR-0037 草稿预览条)。
    4. packages/app-shell/src/console/ai/AiChatPage.tsx:2124 —— AI 会话里的 Publish 按钮;并且在 getRuntimeConfig().features.autoPublishAiBuilds 打开时,由 ChatbotEnhanced 的 autoPublishDrafts 自动触发(AiChatPage.tsx:2220),无人点击。

    packages/plugin-chatbot/src/ChatbotEnhanced.tsx 只声明 onPublishDrafts 回调,自己不 fetch —— 不是独立调用方。

    搜了但没命中: objectui e2e/、apps/、examples/ 搜 publish-drafts 零命中。

    未普查

    cloud 仓 —— 本会话未挂载该仓,没搜。


    2. 每个调用方各自持有什么能力

    静态阅读(除标注处外)。

    Studio 两处(PackagesPage、StudioDesignSurface):
    挂在 studio app 下 —— packages/platform-objects/src/apps/studio.app.ts:54 requiredPermissions: ['studio.access'];Packages 页是同文件 :114 的 componentRef: 'developer:packages'。app 级 requiredPermissions 由 /me/apps 落地(packages/rest/src/rest-server.ts:2621 / :2672:不满足就把整个 app 从列表里丢掉)。所以进得了这两处 UI 的人必然持有 studio.access。

    出厂权限集里 studio.access 只有 admin_full_access 一个集合授予(packages/plugins/plugin-security/src/objects/default-permission-sets.ts:120-129),而这个集合同时带 manage_users、manage_metadata、manage_platform_settings、manage_sharing、setup.access、setup.write。

    Console 三处(Home 横幅、DraftPreviewBar、AiChatPage):
    不在任何 app 的 requiredPermissions 之下,代码里也没有任何能力判断。HomePage.tsx:128-160 的 PendingDraftsBanner 唯一的显示条件是 client.listDrafts?.({}) 返回条数大于 0。

    那个 listDrafts 打的是 GET /meta/_drafts —— 实测:零能力调用方({userId:'u_portal', systemPermissions:[]})得 200,正常返回 draft 列表。所以 Console 这三处对任何已认证用户都可达。

    出厂的其余权限集:

    权限集 systemPermissions 出处
    admin_full_access manage_users, manage_metadata, manage_platform_settings, manage_sharing, setup.access, setup.write, studio.access default-permission-sets.ts:120-129
    organization_admin manage_org_users, setup.access, setup.write default-permission-sets.ts:190(注释明说故意不给 studio.access 与 manage_metadata)
    member_default 无(该集合内没有 systemPermissions 键) default-permission-sets.ts:313 起
    showcase_ops(示例) setup.access, showcase.export_data examples/app-showcase/src/security/permission-sets.ts:190

    ⚠️ 两个前提必须说在前面:

    1. 以上是出厂默认。部署方可以把任何能力授给任何人,所以第 3 条的「会拦下谁」只在出厂默认权限集这个前提下成立。
    2. UI 可达性不等于路由可达性。 实测:零能力调用方 GET /packages 得 200(拿得到 package id 清单),然后 POST /packages/:id/publish-drafts 得 200。整条链不需要任何 UI、不需要看见任何按钮。

    3. 三条候选门各自会 403 掉谁

    前提:出厂默认权限集 + 上面那份调用方清单。

    (a) manage_metadata(与 #6603 已裁的同一道门)

    • 会被拦下的现有调用方:Console 三处(Home 横幅 / DraftPreviewBar / AiChatPage 含自动发布),当持有者是非平台管理员时。具体是 organization_admin 和 member_default 这两类人 —— 这三处今天对他们可达。
    • 不会被拦下:Studio 两处(PackagesPage、StudioDesignSurface)。 进 Studio 已经要 studio.access,出厂只有 admin_full_access 授予它,而该集合同时带 manage_metadata。
    • ⚠️ 结论:(a) 对「Studio 里的 Publish 按钮」一个都不拦。

    (b) setup.write(或同级发布类能力)

    • 持有者:admin_full_access 和 organization_admin。
    • 会被拦下: 只有 member_default 这一类(以及任何未被授予上述集合的用户)。organization_admin 照旧能整包发布。
    • Studio 两处同样一个都不拦。

    (c) 专设一个新的发布能力

    • 出厂没有任何权限集授予它。所以在它被显式写进 admin_full_access(以及/或被 bootstrapSystemCapabilities 播种)之前,它会 403 掉上面全部 5 个现有调用方,包括平台管理员 —— Studio 的 Publish 按钮当天就坏。
    • 先例:manage_sharing 的注释(packages/spec/src/security/capabilities.ts:52-60)写明「Seeded into admin_full_access so existing admin flows are unchanged」。
    • 一旦只播种进 admin_full_access,它对现有调用方的拦截面就与 (a) 完全一致。

    ⚠️ 直接回答那个问题

    (a) 不拦下任何 Studio 调用方;(c)(播种进 admin_full_access 后)同样不拦。 两者唯一改变的,是 Console 那三处对非平台管理员的可达性。(b) 连 organization_admin 都不拦,只拦 member_default。


    4. 兄弟路由 POST /metadata/_migrate-stored 的机制与判据

    静态阅读 + 实测信封。 两道门叠着:

    ① 域级匿名门 —— packages/runtime/src/domains/meta.ts:163-177,handleMetadataRequest 的第一条语句:

    if (shouldDenyAnonymous({ userId: ec?.userId, isSystem: ec?.isSystem })) {
        return { handled: true, response: deps.error(ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, { code: ANONYMOUS_DENY_CODE }) };
    }

    实测状态码 401。

    ② 路由级能力门 —— packages/runtime/src/domains/meta.ts:460-476,判据就一行:

    const ec: any = _context.executionContext;
    if (!ec?.isSystem && !new Set< string >(ec?.systemPermissions ?? []).has('manage_metadata')) {
        return { handled: true, response: deps.error('Rewriting stored metadata requires the `manage_metadata` capability.', 403) };
    }

    实测信封:{ code: 'PERMISSION_DENIED', httpStatus: 403 },且 migrateStoredMetadata 调用次数 0。

    照抄时要一起抄的三个姿态:

    1. 位置 —— 门在 deps.resolveService(_context, 'protocol') 之前。注释写明理由:不让调用方拿 501/200 的差别去探测部署装没装这个能力。
    2. isSystem 旁路 —— 引擎自调用绕过,与 actionPermissionError 一致。
    3. 被 pin 住 —— packages/runtime/src/domains/meta-migrate-stored.test.ts:44-93 四个用例:无能力 403 且被调函数一次没进、持能力放行、isSystem 旁路、匿名 401。

    对照面(实测 grep): packages/runtime/src/domains/packages.ts 全文 739 行,systemPermissions|isSystem|manage_metadata|FORBIDDEN 命中数 0;并且没有 shouldDenyAnonymous。全仓 shouldDenyAnonymous 的调用点是 /meta、/actions、/automation、/ai、/security 五个域加 REST 层 —— /packages 不在其中。packages/qa/dogfood/test/authz-conformance.matrix.ts 里有 anonymous-deny-meta / -actions / -automation 三条,没有 /packages 的任何条目。


    5. ADR-0045 那一环:清 app._unpublished 发生在哪一步、能否分开

    发生在哪一步 —— 实测

    单次 publish-drafts 调用内的真实调用顺序(探针记录的 trace):

    1. publishPackageDrafts(app.hr)                                  ← draft 行转正
    2. getMetaItem(seed/demo_rows)                                   ← seed 读回,随后走 applyPublishedSeeds 灌行
    3. getMetaItems(type=app)                                        ← 找该 package 名下的 app
    4. saveMetaItem(app/hr_console _unpublished=false)               ← ADR-0045 发布门被清掉
    ### saveMetaItem 实参 ### {"type":"app","name":"hr_console",
                              "item":{"name":"hr_console","_unpublished":false,"hidden":true},
                              "packageId":"app.hr"}
    ### 响应 ### 200,data.unhiddenApps = ["hr_console"]
    

    即:第 4 步,严格在 draft 转正与 seed 灌入之后。

    静态阅读(packages/runtime/src/domains/packages.ts:230-301):这一步在自己的 try/catch 里,失败只写 result.unhideError 加一条 logger.error,路由照样返回 200;成功的名字累积进 result.unhiddenApps。

    能否与 schema 激活 / seed 灌入分开

    • 在这条路由上不能。 没有任何开关或参数让调用方只做其中一件事;三件事在一次 POST 里绑死。(实测:body 里除 actor 外没有被读的键;静态阅读 packages.ts:160-172 确认只从 body 取 actor。)

    • 在代码结构上是分开的。 第 4 步只用 getMetaItems / saveMetaItem 两个 meta 原语,不读前两步的返回值,是一个可独立摘出的块。

    • 而且这一步单独另有一条门径 —— 路由自己的错误文案就写着(packages.ts:291):可以直接 PUT /meta/app/:name 带 {"_unpublished": false} 补做。实测:零能力调用方打 PUT /meta/app/hr_console 带 {_unpublished:false} 得 200、saveMetaItem 被调用 1 次;同一条路由匿名打得 401。

      也就是说,在当前 origin/main(c3f491626)上,单独做 ADR-0045 翻转这条路径同样没有能力门 —— finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 裁下的 manage_metadata 门尚未落到 main(domains/meta.ts 全文 manage_metadata 唯一命中就是 _migrate-stored 那处)。

    ⚠️ 这条里未实测的部分(不拿静态阅读冒充)

    • seed 到底落了多少行:未实测。 我的 double 缺 metadata.getObject,seedApplied 返回 {success:false, error:"this.metadata.getObject is not a function"}。已实测到的是:applyPublishedSeeds 被这个零能力调用进去了、并已读回 seed body(trace 第 2 步);没实测真实 driver 上的插入行数。

    PD #10 —— 普查中撞到的同形无门写面(按令记在此,未另立卡)

    同一次实测、同一个零能力调用方 {userId:'u_portal', systemPermissions:[]}:

    • POST /packages/:id/discard-drafts → 200,discardPackageDrafts 被调用。这是破坏性的:丢掉该 package 名下每一条 pending draft。
    • GET /packages/:id/export → 200,把该 package 名下 27 种 metadata 类型整包读出来(trace 里 27 次 getMetaItems)。
    • GET /packages → 200(package id 枚举面,正是上面那条攻击链的第一步)。
    • PUT /meta/app/:name 带 {_unpublished:false} → 200(见第 5 条)。

    其余 /packages 写面在我的 double 下没跑到底:revert 503 / rollback 501 / duplicate 501 / adopt-orphans 501 / enable·disable·DELETE /:id·PATCH /:id 500 / POST /packages 400。⚠️ 这些非 200 全部是我的 double 缺服务方法,不是授权拒绝 —— packages.ts 全文授权判据数为 0,所以它们同样没有门,只是未实测到底,不要读成「这些已经被拦住了」。


    探针

    临时 vitest 文件 packages/runtime/src/zz-probe-7023.test.ts(一次性 worktree 内),驱动 HttpDispatcher.handlePackages / handleMetadata / dispatch,protocol 侧全 vi.fn() 双档并记录调用顺序。上面所有 ### 行为其原始 stdout。已删除、未提交、未推分支、worktree 已移除。

    本评论不含修法建议 —— 门的选择留给维护者。

    🤖 Generated with Claude Code


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    普查的四条 out-of-scope 发现已分流(PD #10)

    普查评论(5231013640)末尾记了四条不属于本卡的发现,现分流如下,本卡范围不变:

    发现 去向
    POST /packages/:id/discard-drafts 零能力 200(破坏性) #7033
    GET /packages/:id/export 零能力 200 #7033
    GET /packages 零能力 200(枚举面) #7033
    PUT /meta/app/:name 带 {_unpublished:false} 零能力 200 #7019(已覆盖,未另立卡)

    前三条合并为一张卡的理由

    它们与本卡 publish-drafts 共用同一根因(/packages 域级无授权判据 + 不在 authz-conformance.matrix.ts 里)和同一修法。拆成三条会让三个 PR 各自在 packages.ts 里加一段形状略有差异的门。#7033 记的是那条结构事实:packages.ts 739 行授权判据命中数 0,且该域没有 shouldDenyAnonymous,而 /meta /actions /automation /ai /security 五个域都有。

    第四条为什么不另立卡(已核,非假设)

    普查走的是 dispatcher 管线(packages/runtime/src/domains/meta.ts),不是 packages/rest/src/rest-server.ts。#7027(#6603)只在 rest 侧给 PUT /meta/:type/:name 落门,不碰 dispatcher —— 所以这一条不会被 #7027 关掉。

    但它正落在 #7019 的范围内:#7019 的三条肢体之一就是 dispatcher 的 PUT(实测在 meta.ts:152-159,卡里原记的 :264 是读路径,已在 #7019 更正)。#7019 落地后该路由对零能力调用方即为 403。⇒ 重复,不另立卡。

    本卡状态未变

    维护者现行指令:先只派普查,不落门。普查已交付,三条候选门各自会 403 掉谁的实测对照在上一条评论里。本席不代裁门,等维护者定向。

    ⚠️ 落门前请注意普查自陈的覆盖缺口:cloud 仓未挂载(add_repo 两次被拒),调用方普查不覆盖该仓 —— 任何门都可能 403 掉一个未被普查到的调用方。


    Generated by Claude Code

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    认领(dev agent)


    Generated by Claude Code

  4. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    认领 + 状态:本卡正在被处理,不是无人管

    此前只跑了只读普查(评论 5231013640),普查按约定未认领 issue —— 所以它看起来无人认领。现补上认领并说明归属,消除误会。

    归属:#7023 与 #7033(/packages 域整体无授权)同根因、同文件,由一个 PR 一起关。该 PR 正在飞:

    • 分支 claude/issue-7033-packages-authz(checkpoint 97c875715,PM 在首个 dev 撞账号 session 限额后打的 wip 备份并已推送)
    • 现有 dev 正从该 checkpoint 续做收尾,PR 将带 Fixes #7033 且 Fixes #7023
    • session session_017uFVNMmTxLpmfQYiuKM1Yx

    #7023 对应的门(维护者已裁):POST /packages/:id/publish-drafts 及其破坏性同族 discard-drafts 要求 has('manage_metadata');读路由(GET /packages、export)走域级 shouldDenyAnonymous + D4 读豁免常量。每个 transport 都上门(REST + dispatcher)。

    维护者现行裁定:保持 #7023 与 #7033 合并、整单顶到最高优先级(插队),绿了立即翻牌 —— 不拆。

    草稿 PR 一开我会回帖贴号。


    Generated by Claude Code

  5. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    已随 PR #7083 合入,本卡关闭

    /packages 域授权门已落地 main(b295e4b36),#7033 与 #7023 由同一 PR 一起关。

    #7023 对应的落地事实(实测 origin/main):packages/runtime/src/domains/packages.ts 现有 7 处 manage_metadata/shouldDenyAnonymous 命中,POST /packages/:id/publish-drafts 及其破坏性同族 discard-drafts 要求 manage_metadata,读路由走 D4 读集 + 全域 shouldDenyAnonymous,两 transport(dispatcher + REST direct-mount)各一份门,门在协议/服务解析之前(拒绝不写不删、防指纹)。pin 已进 packages-capability-gate.test.ts 与 package-envelope.conformance.test.ts。

    遗留盲区(已在 PR 正文与 changeset 写明):cloud 仓未挂载(add_repo 两次被拒),调用方普查不覆盖该仓 —— 落地后若在 cloud 内发现今天不持 manage_metadata/D4 读集的生产调用方,可能被此门 403,需补一次 cloud 普查复核。此外 #7020 记录的「门能力集 ≠ ADR-0106 D4 掩码豁免集」对齐方向仍归维护者。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions