Skip to content

test(dogfood): cover /actions and /automation in the anonymous-deny proof artifact (#5570) - #5631

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5570-anonymous-deny-proof
Aug 5, 2026
Merged

test(dogfood): cover /actions and /automation in the anonymous-deny proof artifact (#5570)#5631
baozhoutao merged 1 commit into
mainfrom
claude/issue-5570-anonymous-deny-proof

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5570

test-only。请 PM 打 skip-changeset 标签 —— .github/workflows/pr-automation.yml 的 changeset 门把 "tests-only" 明写为 route 2 且标注 PREFERRED;packages/qa/dogfoodprivate: true,不发版,不产生 changeset 输入。(我已自行贴上该标签以免 CI 无谓变红,如与 PM 流程冲突请摘掉。)

前提复核(先证后改,rule 6)

分诊席 19:59Z 核过一次;我在动手时刻的 origin/main(7e58212) 上重核:前提成立showcase-anonymous-deny-surfaces.dogfood.test.ts 改动前共 4 条用例,覆盖面只有 /meta ×2 与 /data/showcase_private_note ×2,grep 全文无 /actions、无 /automation。而 authz-conformance.matrix.ts:90 把该文件列为 anonymous-deny-meta 行的 proof 工件,该行所在的注释块声明的是 #2567「anonymous-deny posture is UNIFORM across HTTP surfaces」。

这次补的证据比 #5569 自己的集成用例多出什么

#5569dispatcher-plugin.anonymous-gate.integration.test.ts 最后一条用例自己写明了它做不到的那半:

The cross-surface contrast that made this a p0 — /data answering 401 while /actions answered 200 in the SAME process — is not provable on this boot: @objectstack/rest owns /data and /meta and the dispatcher plugin mounts neither, so there is no second surface here to compare against.

本 PR 补的正是这半:showcase 真启动里,一个进程、两条注册路径(@objectstack/rest/data + /meta;dispatcher-plugin.ts/actions + /automation 直接挂到 host server)四个面并排断言。那条注册路径的分叉就是 #5519 之所以发生的原因。

落点

三个文件,全部在 packages/qa/dogfood/test/,packages/runtime/** 一行未动(git diff --stat -- packages/runtime 为空)。

1. showcase-anonymous-deny-surfaces.dogfood.test.ts(+7 例,4 → 11)

匿名请求 断言
POST /actions/showcase_task/showcase_mark_done/anon-probe-id 401
POST /automation/showcase_reassign_wizard/trigger 401
GET /automation 401
DELETE /automation/showcase_reassign_wizard 401

对象 / action / flow 名全取自 showcase 现有声明:showcase_mark_doneshowcase_task 上的 type: 'script' action(body 走 api.write,即 #5519 实测匿名拿到 200 并真的落了写的那一条);showcase_reassign_wizard 是 showcase 已声明的 flow。

recordId 刻意用不存在的 id:门是 handleActionsRequest第一条语句,匿名请求在任何对象 / action / 记录被解析之前就被拒。若非得先造一条真记录才能拿到 401,那说明门已经挪到了 lookup 后面。

再加两条已认证对照,它们是这批用例牙齿的来源(见下)。

2. 信封:同一个 code 与 message,两种 wrapper —— 如实钉住

这里有一条与派发单预设不符、必须申报的事实。派发单要求「信封断言与套件内 /data/meta 现有条目逐字同形(同一 error/code 键)」。实测两族的 body 形状并不同:

GET  /meta          -> {"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}
GET  /data/...      -> {"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}
POST /actions/...   -> {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}
GET  /automation    -> {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}

@objectstack/restANONYMOUS_DENY_BODY 的扁平信封,dispatcher 走它自己的 wrapper。这是 ADR-0112 修正案记录在案的两个 live 信封,不是本单引入的。

所以我没有硬凑「逐字同形」,也没有写 ?? 容忍链去把两种形状读成一种 —— 那正是 rule 5 要避免的宽容消费者。做法是:每族按各自声明的形状显式断言,再单独钉住真正统一的那层语义(同一 status / 同一 code / 同一 message),并注明 wrapper 分叉是已知平台事实。这样任何一族哪天变形都会红,而不是被容忍读悄悄吃掉。

顺带发现 @objectstack/coreANONYMOUS_DENY_BODY 注释自称 "The single 401 body shape every seam returns",与上表不符 —— 已另行立单(见文末),未在本 PR 修。

3. authz-conformance.matrix.ts + .test.ts —— 登记惯例随行

#2567 Phase 2 现有惯例(surface 行 = enforcement + proof + covers,并进 HIGH_RISK)补两行:

  • anonymous-deny-actionscovers: ['actions:domains/actions.ts:anonymous-gate']
  • anonymous-deny-automationcovers: ['automation:domains/automation.ts:anonymous-gate']

并补两条 PROBES(形状与既有 buildMcpBridge(context-threaded) / stdio-principal-bound 这两条 gate pin 一致,而非 surface 探针)、一条 (h) bites 用例。删掉任一侧的门 → key 消失 → 覆盖它的行 STALE → CI 红。

这一步比 issue 的「约 20 行」建议做得多,理由:只加没有 covers 的行会与紧邻的 #2567 surface 行形成半登记,而 ratchet 才是让 #5519 那类回归在 CI 而非 review 里被抓住的机制。全部限于 packages/qa/dogfood。若 PM 认为超出本单,这三处可单独摘掉,不影响 1 的用例。

反向验证(方向先判后跑)—— 分两阶段,方向不同

预判先写,并且故意不是「一律转红」:

用例 预判
匿名 POST /actions/... 401 → 400(动作真派发,body 撞上 showcase_audit_task_completion 的 hook condition 报错)。红
匿名 /automation ×3 401 → 501(本 boot 未装 @objectstack/service-automation)。红
信封一致性用例
/data/meta ×4 照常绿(归 @objectstack/rest,未触碰)
两条已认证对照 照常绿(.not.toBe(401) 与 501 钉子都不受门影响)
conformance ratchet 阶段 A 绿(见下)

阶段 A —— if (false && shouldDenyAnonymous({...,重建 runtime

Test Files  1 failed | 1 passed (2)
     Tests  5 failed | 15 passed (20)

× anonymous POST of a script action is denied (401)
  AssertionError: anonymous action dispatch must be 401: expected 400 to be 401
× anonymous POST /automation/:name/trigger is denied (401)
  AssertionError: anonymous flow trigger must be 401: expected 501 to be 401
× anonymous GET /automation is denied (401) — the flow inventory stays private
  AssertionError: anonymous flow listing must be 401: expected 501 to be 401
× anonymous DELETE /automation/:name is denied (401) — the destructive one
  AssertionError: anonymous flow deregistration must be 401: expected 501 to be 401
× every denied surface answers the SAME code and message (the wrappers differ)

逐条命中预判,/data/meta、两条已认证对照全绿。

这里有一条值得单独说的:/automation 的「无门答案」是 501 而不是 200,因为本 boot 没装 automation 服务。这恰恰是那条已认证 501 对照用例存在的理由 —— 同一个进程、同一条路由,匿名 401 / 成员 501,两者并排,401 就只可能是门给的。这也是 #5569 刻意把门放在 isServiceServeable 探测之前的原因(否则 401 与 501 的差异会泄露该部署是否挂了 automation)。用例注释里写明了。

conformance ratchet 在阶段 A 保持绿,这是预判之内:if (false && 保留了 shouldDenyAnonymous( 这个 token,而 ratchet 钉的是调用的存在,不是它的活性 —— 与文件里既有的每一条 gate pin 同一性质。两层是互补的,这本身就是 #5570「光有 ratchet 不够、proof 工件必须真跑」的论据。

阶段 B —— 真正删掉调用(shouldDenyAnonymous → 改名)

Tests  3 failed | 6 passed (9)

× is a sound conformance ledger ... + the #2567 surface ratchet holds
    STALE covers — surface no longer in source: actions:domains/actions.ts:anonymous-gate
    STALE covers — surface no longer in source: automation:domains/automation.ts:anonymous-gate
× the real matrix + real discover is sound (baseline lock)
× (h) deleting either /actions or /automation anonymous gate → STALE covers failure (#5519)

还原后 git diff --stat -- packages/runtime 为空。

消费半径扫描

不按被改的包扫,按被改工件的 caller 扫:grep -rn "authz-conformance" 全仓(排除 node_modules)只命中 packages/qa/dogfood 内部,外加 packages/spec/src/contracts/realtime-service.tsplugin-security/src/authz-matrix-gate.test.ts 两处纯注释引用,无代码依赖。proof 工件本身只被 checkLedger 按文件名存在性校验。

实跑记录

pnpm --filter @objectstack/dogfood test       ->  Test Files 85 passed | 1 skipped (86)
                                                  Tests 514 passed | 3 skipped (517)      (main 基线 506 passed,+8 = 本 PR 新增)
pnpm --filter @objectstack/dogfood typecheck  ->  tsc --noEmit,无输出(绿)
node scripts/check-nul-bytes.mjs              ->  OK (scanned 5531 tracked text files; no raw ASCII control bytes)
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' <本 PR 三个文件>  ->  clean

自限

  • 文件面只在 packages/qa/dogfood/test/,三个文件,+210 行 / -0 行。未触碰 packages/runtime/**(防线本体)、未触碰其他 dogfood 套件、未触碰 content/docs/releases/
  • 该套件留在 shared-showcase project(plain bootStack(showcaseStack)),未改 vitest.config.ts、未改 shared-showcase.ts,不增加 boot 开销。用 automation: true 启动 showcase 需要 process.chdir + 三个 connector 插件 + 临时 MetadataPlugin(见 showcase-declarative-endpoints.dogfood.test.ts),那是另一类测试;且在共享 stack 上做真能生效的 DELETE /automation/:name 断言,一旦门回归就会连带污染同 worker 的其他文件。
  • 未新增 changeset(test-only,见文首)。

顺带发现(未在本 PR 修)

@objectstack/coreANONYMOUS_DENY_BODY 注释写着 "The single 401 body shape every seam returns: { error, message }",但 dispatcher 侧五个 seam(ai / meta / security / actions / automation)返回的是 wrapper 形状。已按 Prime Directive #10 单独立单,不在本 PR 修。


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

…roof artifact (#5570)

`authz-conformance.matrix.ts` names
`showcase-anonymous-deny-surfaces.dogfood.test.ts` as the proof artifact for
#2567's "anonymous posture is uniform across HTTP surfaces" claim, but the
suite drove only `/data` and `/meta`. #5519 found the claim false on exactly
the two surfaces it did not drive — the dispatcher-mounted `/actions` and
`/automation` — and the artifact was silent throughout.

PR #5569 built the gate in `packages/runtime`; this is the evidence half.

- six new anonymous cases on the shared showcase boot: POST a `script`
  action, POST `/automation/:name/trigger`, GET `/automation`, DELETE
  `/automation/:name` (all 401), plus the two authenticated contrasts.
- one case pins that all four surfaces answer the same code and message,
  reading each family in its own declared envelope rather than through a
  tolerant `??` chain.
- two matrix rows (`anonymous-deny-actions`, `anonymous-deny-automation`)
  with their `covers` keys, ratchet probes for both gates, and a `(h)`
  bites case, so deleting either gate fails CI as STALE covers.

No `packages/runtime` change: this adds proof, not defence.

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

vercel Bot commented Aug 5, 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 5, 2026 8:57pm

Request Review

@baozhoutao baozhoutao added tests skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 5, 2026 — with Claude
@github-actions github-actions Bot added the size/m label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/dogfood.

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

  • content/docs/permissions/authorization.mdx (via packages/qa/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa/dogfood)

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.

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 21:11
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 4658e57 Aug 5, 2026
30 of 31 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5570-anonymous-deny-proof branch August 5, 2026 21:23
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…velopes (objectstack-ai#5632) (objectstack-ai#5801)

`ANONYMOUS_DENY_BODY` 的注释自称 "The single 401 body shape every seam
returns",但只有 `@objectstack/rest` 的 `enforceAuth` 消费它;dispatcher 侧
五个 domain(ai / meta / security / actions / automation)走
`deps.error(...)`,发的是 wrapper 信封。注释窄化为 REST seam 的形状,并写明
另一半的归属;两个信封均为 live(ADR-0112 2026-07-30 修正案),收敛归
envelope-convergence 线(objectstack-ai#3843 family),本单不动 wire。

conformance:在 PR objectstack-ai#5631 落的匿名面切片同文件里补「每个 seam 的 401 body
属于且仅属于两种已声明形状之一 + 归属映射」,判形状用互斥显式判别,code /
message 直接对 `@objectstack/core` 导出的常量断言,不写 `??` 跨族容忍读。


Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants