Skip to content

「v17」入站 rateLimit 接执行:ApiEndpoint / HttpServer 的 RateLimitConfig 推导为 runtime token bucket 配置,dispatcher 生效(#4686 拆向之一) #4910

Closed

Description

@xuyushun441-sys

Part of #4686。维护者裁决(2026-08-03,PM 会话 session_01Ehu85kbvMcrNTUJjwxvLJ9 经 AskUserQuestion):拆向 —— 入站接执行,出站摘除。本单是入站半边;出站半边(connector 侧摘除)见兄弟子单,两者相互独立、不共享文件面。

事实基础(#4686 立单 + 2026-08-03 复核,C9 改名后)

  • packages/spec/src/shared/http.zod.ts:127 的 RateLimitConfigSchema,三个嵌入点全部从作者面可达:
    • api/endpoint.zod.ts 的 ApiEndpointSchema.rateLimit
    • api/registry.zod.ts 的 ApiEndpointRegistrationSchema.rateLimit
    • system/http-server.zod.ts 的 HttpServerConfigSchema.security.rateLimit
  • 全仓零 runtime reader(spec 之外零命中,阳性对照已验证扫描器有效)。作者写下去解析通过,然后什么也不发生。
  • packages/runtime/src/security/rate-limit.ts 有一个真正在干活的 in-memory token bucket(RateLimitBucketConfig:capacity / refillPerSec / defaultCost),服务入站 dispatcher 的 429 短路,但不 import spec。

范围

把作者面声明接到已存在的执行器上:

  1. 从授权配置推导 bucket 配置(ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686 给的推导:capacity = maxRequests,refillPerSec = maxRequests / (windowMs / 1000) —— 以实际 schema 键为准,开工先核 origin/main 现状)。
  2. 接线三个嵌入点;endpoint 级与 server 级的优先级语义由实现者提出并写进 schema describe + 文档(建议:endpoint 覆盖 server,与同文件既有 security 语义一致,不确定就升级)。
  3. 逐键活性:接线后 RateLimitConfigSchema 的每个键要么被执行器消费、要么按 ADR-0049 退役 —— 不允许接了一半、剩下的键继续静默(未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 战役的 declared = enforced 不变量)。哪些键 bucket 表达不了(如有 burstCapacity / respectUpstreamLimits 一类),逐键实测后处置,不猜。

⚠️ 激活语义 = 本单的 breaking 面

存量栈里写过 rateLimit 配置的,升级 v17 后从「惰性」变「真限流」—— 可能开始吃 429。这是有意的(declared = enforced 的强实现),但必须:

  • ADR-0087 D3 semantic migration 条目 + upgrade guide(走生成管线,不手编);
  • major changeset,FROM → TO 写清「你的配置将开始生效」;
  • v17 窗口内落地(.changeset/pre.json mode:pre / tag:rc 关闭后无 major 通道)。

验收

  • 一条端到端测试:授权 rateLimit 配置经真实 dispatcher 路径产生 429(declared=enforced 探针,先证红:未接线时同配置不限流)。
  • 逐键消费证据或退役登记,无静默残留键。
  • spec 十道 check:* + 全仓 typecheck 绿;三个示例应用 validate 过。
  • ⛔ 不碰 content/docs/releases/;出站/connector 侧零改动(那是兄弟子单的面)。

关联:#4686(父单,裁决记录)、#4684(C9 改名收敛,已关)、ADR-0049、ADR-0087、Prime Directive #10。

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 3, 2026
  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    🔒 认领:PM 循环插派(维护者指示有空位即插;批 9 已回报验收,空位放行本单)
    会话:session_01Ehu85kbvMcrNTUJjwxvLJ9
    分支:claude/issue-4910-inbound-ratelimit-wire
    Worktree:objectstack-issue-4910
    域:domain:cli(跨域触碰 spec 迁移注册表,文件面全量声明)
    文件面:packages/runtime/src/security/rate-limit.ts 及其 dispatcher 调用点(hono/runtime)、必要时 packages/spec/src/shared/http.zod.ts + 三个入站嵌入点(逐键活性处置)、packages/spec/src/migrations/(D3)、spec 生成物、changeset

    与在飞不相交:#4911(integration/connector.zod.ts,且被明令不碰入站面)、批 10(automation/*)。migrations 注册表与 #4911 共享追加点,合并双边保留(#4878 惯例)。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    开工核查结论:停工待裁决(needs_decision)—— 立单事实基础中有一条在 origin/main 上不成立,连带三个嵌入点全部没有可接的执行端

    会话 session_01Ehu85kbvMcrNTUJjwxvLJ9,分支 claude/issue-4910-inbound-ratelimit-wire(worktree 已建,未写任何代码)。基线 origin/main @ 2e284b2546。.changeset/pre.json 已确认仍是 mode: pre / tag: rc,v17 窗口开着。

    ⛔ 本评论不重议「接 vs 摘」的裁决。要问的是裁决之外的新契约问题:「接执行」要接到哪里去 —— 因为核查发现,单子假定「已存在的执行器」并不存在。


    一、被推翻的那条事实

    立单写道:「packages/runtime/src/security/rate-limit.ts 有一个真正在干活的 in-memory token bucket……服务入站 dispatcher 的 429 短路」。

    实测:它零调用点,全仓入站路径没有任何 429 短路。

    $ grep -rn "new RateLimiter(" --include=*.ts --include=*.mjs --include=*.js . | grep -v node_modules | grep -v /dist/
    packages/runtime/src/security/rate-limit.test.ts:23,39,54,65,77,86,97,98   ← 全部是它自己的单测
    (无其它命中)
    
    # 阳性对照(证明扫描器有效):
    $ grep -rn "new DomainHandlerRegistry(" --include=*.ts . | grep -v node_modules
    packages/runtime/src/http-dispatcher.ts:167   ← 真实调用点找得到
    
    # 入站 429:
    $ grep -rn "429" --include=*.ts packages/runtime/src packages/adapters packages/rest/src packages/core/src | grep -v .test.
    packages/runtime/src/security/rate-limit.ts:8   ← 就是那句宣称有 429 短路的注释本身
    packages/rest/src/rest-server.ts:6897           ← THROTTLED 错误码映射,与限流器无关
    

    误判的来源已定位:rate-limit.ts 的注释用现在时描述了一条不存在的执行链 —— 「the dispatcher calls consume(key) … short-circuits with 429」、「tune via DispatcherPluginConfig.rateLimit」(该字段在 dispatcher-plugin.ts:23 的接口里不存在)、「the dispatcher constructs the key from ${ip}:${bucket}」(无此代码)。已单独立单 #4937。

    所以本单的真实形状不是「把声明接到已有执行器」,而是从零建一个入站限流执行点,而执行点的形态、key 形状、存储、配置入口全是新的公共契约决定。

    二、三个嵌入点逐个复核:没有一个有可接的两端

    嵌入点 作者能写吗 有执行端吗 证据
    ApiEndpointSchema.rateLimit ✅ defineStack({ apis }) ❌ 端点本身就不执行 见下
    ApiEndpointRegistrationSchema.rateLimit ⚠️ 仅经 ApiRegistryEntrySchema ❌ api-registry plugin 只在 packages/core/examples/ 装配 #4939
    HttpServerConfig.security.rateLimit ❌ 写不进来 ❌ 零 reader #4938

    第三行值得单独说:stack.zod.ts 没有 server: / httpServer: 键,config-schema.json 里 HttpServerConfig 零命中,settings 也没有对应 manifest。所以这个嵌入点不只是「写下去不生效」,而是根本没有地方写。真正决定端口/CORS 的是 CLI 参数、hono adapter 的 ObjectStackHonoOptions、DispatcherPluginConfig 三份别的形状。

    第一行是真实 boot 证据(不是 grep 推断)。pnpm dev -- --fresh -p 39147,showcase,47 plugins,同一会话 cookie:

    GET  /api/v1/meta/api                  → HTTP 200,两个端点元数据装载完好(path/method/type/authRequired/cacheTtl 齐全)
    GET  /api/v1/showcase/tasks            → HTTP 404 {"error":"Not found"}
    POST /api/v1/showcase/inquiries/purge  → HTTP 404 {"error":"Not found"}
    GET  /api/v1/data/showcase_task?top=1  → HTTP 200   ← 阳性对照,同 cookie 同 prefix
    

    两处断链:① dispatcher-plugin 逐条显式挂载路由,元数据声明的任意路径没有 catch-all,进不了 dispatch();② 即便进得去,handleApiEndpoint(http-dispatcher.ts:1646)第一件事是 typeof metaSvc.matchEndpoint !== 'function' 就返回 { handled: false },而 matchEndpoint 全仓仅 3 处命中、全在 http-dispatcher.ts 自己身上,没有任何元数据服务实现它。已立单 #4936(并波及 authRequired / cacheTtl / inputMapping / outputMapping 等同样零执行的键)。

    结论:验收第一条「授权 rateLimit 配置经真实 dispatcher 路径产生 429」在当前 main 上无法达成 —— 端点根本不经过 dispatcher。硬做只能造一个假的 metadata 替身来喂 matchEndpoint,那正是 AGENTS.md「Boot the real composition with its real services, or do not claim an answer」禁止的答法。

    三、一个连带的好消息:breaking 面比立单设想的小得多

    立单把「存量栈写过 rateLimit 的升级后开始吃 429」列为必须走 ADR-0087 D3 semantic migration + upgrade guide 的理由。按上面的事实:server 级那个键今天没人写得出来(新键 = 无存量),endpoint 级那两个若按下面推荐方案不接线则不发生激活。也就是说,若采纳推荐方案,D3 semantic migration 条目与 upgrade guide 都不需要,一个普通的 feature changeset 即可。这条请一并裁决。


    待裁决(四问,均为裁决之外的新契约问题)

    Q1 — 入站限流执行点建在哪?配置从哪进来?

    选项 长期健康度(本项目) 让 AI 写的元数据难写错
    A. 只加 DispatcherPluginConfig.rateLimit(代码级配置) ❌ spec 三个键仍然零 reader,本单目标未达成;等于给 rate-limit 添第四份形状,#4686 的「三份形状零通路」变四份 ❌ 作者面毫无变化,AI 写的 rateLimit: 继续静默通过
    B. 给 stack.zod.ts 加 server:(承载 HttpServerConfigSchema),dispatcher-plugin 从中构建 RateLimiter ✅ 唯一让 spec 键真正被消费的路线;顺带给现存三份 cors/port 方言一个收敛靶子(契约优先) ✅ 声明即执行:作者写 server.security.rateLimit 就真限流,写错的值由 Zod 在 build 期打回
    C. 入站三个嵌入点全部先 ADR-0049 退役,执行点建好后再引入 ⚠️ 与 2026-08-03「入站接执行」裁决相反,仅在 A/B 均被否时才成立 ✅ 退役墓碑会响亮拒绝,但代价是砍掉一个方向正确的声明面

    推荐 B。 理由(两轴):长期健康度上,只有 B 消除了「声明与执行各说各话」的根因,并把 port/cors/trustProxy 这些今天散在三处的服务器配置收进一份契约 —— 这正是 Prime Directive #12 的契约优先;A 是典型的「今天最便宜」,它的长期成本是把 #4686 记录的三份形状变成四份,并让 spec 的三个键永久停在 declared ≠ enforced。让 AI 少写错这一轴上 B 也更强:B 之后,AI 写下的限流配置要么生效、要么被 Zod 在 build 期拒绝,不存在「解析通过然后什么也不发生」的中间态,而 A 恰恰保留了这个最容易藏错的中间态。B 的代价诚实说明:新增一个公共 stack 键是协议级动作,需要 gen:schema + authorable-surface + 文档,并且要决定它与 CLI -p 的优先级。

    Q2 — endpoint 级两个嵌入点(前置依赖 #4936)怎么办?

    推荐 B。 长期健康度上,C 有实实在在的反噬风险:apis: 整面的存废还没裁(#4936 三选一),此刻单独退役它的 rateLimit,若 #4936 日后选「接上执行」,就要把刚立的墓碑再刨掉重新引入 —— 退役-再引入的来回本身就是 ADR-0049 想避免的动荡。B 不违反「declared = enforced」的精神:静默指的是没人知道、没人跟踪,而 #4936/#4939 已是带实测证据的在册单据,这是「响亮的缺席」而非静默。若维护者要零容忍,C 是可执行的,请明示。

    Q3 — 限流 key 的形状与信任模型(安全契约,#4686 未给)

    • A. 按 IP(取 X-Forwarded-For):⚠️ 有害。trustProxy 本身是 system/HttpServerConfigSchema 九个键全仓零 reader,且没有任何作者面入口(defineStack 无 server:,不在 config-schema) #4938 的零 reader 键,现在无处可取;不接 trustProxy 就等于无条件信任一个可伪造的头 —— 攻击者改个头即绕过限流,还能伪造成他人 IP 把别人的桶顶爆,净增攻击面。
    • B. 按已解析 principal(executionContext):认证流量精确,但匿名流量(凭据填充/爆破,恰恰最需要限流)全挤一个桶,不是形同虚设就是误伤。
    • C. principal 优先,匿名回落到 IP;且 IP 仅在 trustProxy 显式声明时取转发头,否则取 socket 远端地址(与 Q1-B 的 server: 键联动落地 trustProxy)。

    推荐 C,并把 key 形状写进 schema describe + 文档。两轴同向:长期上它一次性把 trustProxy 从死键变成活键;AI 侧它让「限流按什么维度生效」成为声明出来的、可被 review 的事实,而不是埋在实现里的默认。

    Q4 — 计数存储:in-memory 还是 ADR-0069 D2 的共享计数器?

    推荐 B。 长期健康度:ADR-0069 D2 已经为同一问题定过一次调,PD#13 说「an accepted ADR binds until a superseding ADR says otherwise」—— 新建的入站限流器再选 A,等于在同一仓里对同一问题给出两个相反答案,并且是 #4769/#4771/#4772 那批「启动期读注册表」教训的正下方(惰性解析范式现成)。AI 侧:B 让「限流在集群里到底生效没有」有一个可观测的启动期结论,而不是一个看起来健康、实际按实例数打折的静默偏差。


    裁决后我可以立即按选定组合开工。若维护者更倾向把 Q1-B 的 server: 新键单独立单、本单缩为「执行点 + 派生函数 + 测试」,也请一并说明,我按拆法执行。

    新立单据(均未认领):#4936(声明式 apis: 全链路零执行)、#4937(rate-limit.ts 零调用点 + 注释失实)、#4938(HttpServerConfigSchema 九键零 reader 且无作者面)、#4939(ApiRegistry 从未装配)。


    Generated by Claude Code

  4. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 3, 2026
  5. removed their assignment
    on Aug 3, 2026
  6. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    维护者裁决(2026-08-03,经 AskUserQuestion)+ PM 按既有 ADR 定的一项 —— 四问全部落定

    前提修正已接受:执行器不存在,本单是从零建 seam,不是接线。在此前提上:

    问 裁决 要点
    Q1 seam B stack.zod.ts 新增 server: 键,dispatcher-plugin 从中构建 RateLimiter。⚠️ 执行约束:server: 只承载被消费的键(起步 = security.rateLimit + trustProxy)——不许为接一个键顺车激活 #4938 的八个死键,declared = enforced 对新键同样成立;server: 与 CLI -p 的优先级由实现者提出写进 describe + 文档
    Q2 endpoint 级 B 本单只做 server 级;ApiEndpointSchema.rateLimit / ApiEndpointRegistrationSchema.rateLimit 显式登记「已知未接线,由 #4936 跟踪」——apis: 整面存废随 #4936 裁,不在本单预支
    Q3 key 信任模型 C principal 优先、匿名回落 IP;IP 仅在 trustProxy 显式声明时取转发头,否则 socket 远端。trustProxy 就此变活键;key 维度写进 schema describe
    Q4 存储 B(PM 按 ADR-0069 D2 + PD#13 直接定,未升级) kernel-cache 共享计数器,惰性解析范式(createLazyCacheRateLimitStorage);in-memory 仅显式回落,首次降级按 Degradation log levels 说清后果

    附带确认:按 B 落地无激活语义(server: 是新键,存量栈无人写过)→ 无需 D3 semantic migration 与 upgrade guide 条目;新公共 stack 键走 gen:schema + authorable-surface + 文档全套。

    needs-user-decision 摘除,重新入队(父单 #4686 的 pm:queue 使其可派)。下一空位按序放行(当前顺位:#4876 → 批 12 → 本单)。


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 3, 2026
  8. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    🔒 重新认领(四问裁决已齐,批 10 验收后空位放行)
    会话:session_01Ehu85kbvMcrNTUJjwxvLJ9
    分支:claude/issue-4910-inbound-ratelimit-seam(新 slug——前一轮零代码交还,远端无旧分支)
    Worktree:objectstack-issue-4910-seam
    域:domain:cli × domain:spec(跨域单认领,文件面全量声明)
    文件面:packages/spec/src/stack.zod.ts(新 server: 键)+ 新窄形状 schema、packages/runtime/src/security/rate-limit.ts(接真实调用方 + 修失实注释)、dispatcher-plugin(hono/runtime 侧接线)、plugin-auth 的 kernel-cache 存储范式复用、spec 生成物与台账、changeset

    按 18:0xZ 落档的四问裁决执行(Q1=B 带「只承载被消费键」硬约束 / Q2=B / Q3=C / Q4=B),⛔ 不再重议。与在飞批 12(automation/etl)、#4876 待派(ui/dashboard)文件面不相交。


    Generated by Claude Code

  9. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    ✅ 验收通过 —— PR #5006(挂轨中)

    对照实况复核(28 文件 / +2523;⛔ endpoint/registry/connector/releases 零触碰):

    四问裁决逐条落实:

    两个顺路修真的 declared≠enforced:#4937 注释成真(带调用点可查);IHttpServer.use() 在 Hono 适配器里原是 no-op({} as any + 无条件 next())——现为真 seam,有 216 行专项测试。

    RED-first e2e 是范本:第一套件跑的就是 origin/main 形状(同预算无接线 → 断言无 429),第二套件 8 例含跨插件路由门控、OPTIONS 不计量、逐 principal 分桶。

    已知限制(如实声明):composeStacks 静默丢弃非数组顶层键——server: 在组合栈路径会被丢,单栈路径正常;且今天就在丢 api:(含 enforceProjectMembership 403 门)。已立案 #5005(转 needs-user-decision:合并语义要一次裁决;「未处理键必须 warn」的部分是恢复不变量,可先行)。

    落地后:#4937 随 Fixes 关单;父单 #4686 两子单齐,总结关闭;#4938 取舍面稳定,补两轴分析请裁。


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchprotocol:breaking

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions