发现于 #5570 (PR #5631 )的实跑取证 —— 该 PR 首次把四个 HTTP 面的匿名 401 body 在同一个 showcase 进程里 并排取了下来。基线:origin/main 7e58212d1。
Filed unassigned,无标签,待分诊席分域。不在 PR #5631 内修 (那是 test-only 补证据单)。
事实
packages/core/src/security/anonymous-deny.ts:42 的注释:
/** The single 401 body shape every seam returns: `{ error, message }`. */
export const ANONYMOUS_DENY_BODY = {
error: ANONYMOUS_DENY_CODE,
message: ANONYMOUS_DENY_MESSAGE,
} as const;
「every seam」是假的。实测(showcase 真启动,匿名,同一进程):
面
owner
401 body
GET /meta
@objectstack/rest enforceAuth
{"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}
GET /data/...
同上
同上
POST /actions/...
runtime domains/actions.ts
{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}
POST /automation/.../trigger
runtime domains/automation.ts
同上
GET /automation
同上
同上
DELETE /automation/...
同上
同上
只有 @objectstack/rest 消费 ANONYMOUS_DENY_BODY(rest-server.ts:1961 res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY))。dispatcher 侧的 seam —— domains/ai.ts、domains/meta.ts、domains/security.ts,以及 PR #5569 新加的 domains/actions.ts、domains/automation.ts —— 走的都是 deps.error(ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, { code: ANONYMOUS_DENY_CODE }),即 dispatcher 自己的 wrapper。
语义层面三者一致(401 / UNAUTHENTICATED / 同一条 message),分歧只在 wrapper 。
为什么这不只是注释问题
消费者要写两条读法。 读 body.error.code 的客户端在 /data 上拿到 undefined(那里 error 是 CODE 字符串本身);读 body.error 当字符串的客户端在 /actions 上拿到一个对象。fix(runtime): deny anonymous callers on /actions and /automation (#5519) #5569 自己的集成用例就写了 body?.error?.code ?? body?.error?.details?.code —— 一条 ?? 容忍链,正是 AI 生成的元数据/客户端代码最容易把错误藏进去的形状。
注释会误导下一个作者(尤其是 AI)。 一个 as const 常量,docstring 明说自己是「every seam」的唯一形状,读者没有理由去逐个 seam 复核。PR test(dogfood): cover /actions and /automation in the anonymous-deny proof artifact (#5570) #5631 是复核了才发现的。
这是安全语义的出口。声明与实际不符,正是 Prime Directive chore: version packages #10 的形状,只不过发生在注释层 。
与既有工作的关系
ADR-0112 修正案(2026-07-30,Decide when the client's three-location code probe retires — ADR-0112 D5 says "after batch 3", the code now says "legacy-server fallback, NOT debt" #4007 )已把「扁平信封 vs wrapper」记录为两个 live 信封 ,并明说「retiring the flat shape itself belongs to the envelope-convergence line(Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675 { error: '<string>' } #3843 family),not this ADR」。所以分歧本身是已知的、有归属的 。
Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675 { error: '<string>' } #3843 已 closed(completed),其扫描面是 service-settings / service-datasource / external-datasource-routes / package-routes,没有覆盖 ANONYMOUS_DENY_BODY 这条 auth 主干 。
因此这是该条线上一个仍然活着、且未登记 的具体实例,不是 Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675 { error: '<string>' } #3843 的重复(已按关键字 + 文件路径搜过 open issues:ANONYMOUS_DENY_BODY 零命中,envelope/信封 标题命中 driver-mongodb 带着 #5328 的同一条缝:形状错误的 $between / 非数组 $and、$or / 非对象 $not 全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346 、GET /meta/:type/:name answers two different body shapes on the same request — the cached branch (the DEFAULT) returns the bare document, the non-cached branch returns the spec-declared { type, name, item } envelope #5563 两条均为其他面)。
处置建议(未预设,留给分诊)
按代价从小到大:
只修注释 —— 把 docstring 从「every seam」窄化为「the REST seam's shape」,并指向 dispatcher 那半。代价最小,消除误导,但 ?? 容忍链留在原地。
让 dispatcher 侧也发扁平信封 (或反过来让 rest 发 wrapper)—— 真正收敛到一种形状。属于 breaking wire change,应挂在 envelope-convergence 线上统一决策,不宜由本条单独定。
加一条 conformance 用例 ,把「每个 seam 的 401 body ∈ 已声明的两种形状之一,且 code/message 一致」钉住,让第三种方言出现时 CI 红。
倾向 1 + 3 :1 立即止损(注释是唯一在说谎的东西),3 防止分歧从两种扩成三种;2 的时机取决于 envelope-convergence 线何时排期,不该由本条抢跑。
PR #5631 已在 showcase-anonymous-deny-surfaces.dogfood.test.ts 里把这两种形状各按自身声明显式断言 (刻意不写 ?? 跨族容忍读),并单独钉住 code/message 的一致性 —— 相当于把 3 的一个切片先落在了匿名面上;但那只覆盖匿名 401,不覆盖全部 seam 的全部错误。
关联:#5570 、PR #5631 、PR #5569 、#5519 、ADR-0112(D5 + 2026-07-30 修正案)、#3843 、#4007 。
Generated by Claude Code
发现于 #5570(PR #5631)的实跑取证 —— 该 PR 首次把四个 HTTP 面的匿名 401 body 在同一个 showcase 进程里并排取了下来。基线:
origin/main7e58212d1。Filed unassigned,无标签,待分诊席分域。不在 PR #5631 内修(那是 test-only 补证据单)。
事实
packages/core/src/security/anonymous-deny.ts:42的注释:「every seam」是假的。实测(showcase 真启动,匿名,同一进程):
GET /meta@objectstack/restenforceAuth{"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}GET /data/...POST /actions/...domains/actions.ts{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}POST /automation/.../triggerdomains/automation.tsGET /automationDELETE /automation/...只有
@objectstack/rest消费ANONYMOUS_DENY_BODY(rest-server.ts:1961res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY))。dispatcher 侧的 seam ——domains/ai.ts、domains/meta.ts、domains/security.ts,以及 PR #5569 新加的domains/actions.ts、domains/automation.ts—— 走的都是deps.error(ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, { code: ANONYMOUS_DENY_CODE }),即 dispatcher 自己的 wrapper。语义层面三者一致(401 /
UNAUTHENTICATED/ 同一条 message),分歧只在 wrapper。为什么这不只是注释问题
body.error.code的客户端在/data上拿到undefined(那里error是 CODE 字符串本身);读body.error当字符串的客户端在/actions上拿到一个对象。fix(runtime): deny anonymous callers on /actions and /automation (#5519) #5569 自己的集成用例就写了body?.error?.code ?? body?.error?.details?.code—— 一条??容忍链,正是 AI 生成的元数据/客户端代码最容易把错误藏进去的形状。as const常量,docstring 明说自己是「every seam」的唯一形状,读者没有理由去逐个 seam 复核。PR test(dogfood): cover /actions and /automation in the anonymous-deny proof artifact (#5570) #5631 是复核了才发现的。与既有工作的关系
codeprobe retires — ADR-0112 D5 says "after batch 3", the code now says "legacy-server fallback, NOT debt" #4007)已把「扁平信封 vs wrapper」记录为两个 live 信封,并明说「retiring the flat shape itself belongs to the envelope-convergence line(Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675{ error: '<string>' }#3843 family),not this ADR」。所以分歧本身是已知的、有归属的。{ error: '<string>' }#3843 已 closed(completed),其扫描面是service-settings/service-datasource/external-datasource-routes/package-routes,没有覆盖ANONYMOUS_DENY_BODY这条 auth 主干。{ error: '<string>' }#3843 的重复(已按关键字 + 文件路径搜过 open issues:ANONYMOUS_DENY_BODY零命中,envelope/信封标题命中 driver-mongodb 带着 #5328 的同一条缝:形状错误的$between/ 非数组$and、$or/ 非对象$not全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346、GET /meta/:type/:nameanswers two different body shapes on the same request — the cached branch (the DEFAULT) returns the bare document, the non-cached branch returns the spec-declared{ type, name, item }envelope #5563 两条均为其他面)。处置建议(未预设,留给分诊)
按代价从小到大:
??容忍链留在原地。倾向 1 + 3:1 立即止损(注释是唯一在说谎的东西),3 防止分歧从两种扩成三种;2 的时机取决于 envelope-convergence 线何时排期,不该由本条抢跑。
PR #5631 已在
showcase-anonymous-deny-surfaces.dogfood.test.ts里把这两种形状各按自身声明显式断言(刻意不写??跨族容忍读),并单独钉住 code/message 的一致性 —— 相当于把 3 的一个切片先落在了匿名面上;但那只覆盖匿名 401,不覆盖全部 seam 的全部错误。关联:#5570、PR #5631、PR #5569、#5519、ADR-0112(D5 + 2026-07-30 修正案)、#3843、#4007。
Generated by Claude Code