Repository navigation
17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040
Description
Activity
阶段二设计文档(设计先行,PM 复核通过)
出品:执行器车道 PM 会话
session_01EYGdmvWP1ieZSLqvAW6uyd派发的只读设计 agent;设计基线origin/main@70c0769(2026-08-04)。PM 复核结论:裁决继承完整(#4910-Q1=B/Q3=C/Q4=B、#4936 响亮拒绝→翻转)、词表冻结假设贯穿全文、spec 触碰件全部标注 [转回 spec 车道]、未决项列而不猜。维护者否决窗口 —— 以下三处在 E 系列子单派发前需明确认可(不阻塞今天的任何事,阶段一在飞):
- §1 推荐 C(
IHttpServer.setFallbackHandler?契约扩面)vs 备选 A(通配路由,带顺序病灶); - §3.3
cacheTtl语义为本设计所定(GET-only、principal 入键为安全红线、kernel-cache 存储); - §7-3
script/proxy维持 publish 拒绝待单独立卡(proxy是 SSRF 面,须安全裁决)。
子单(E1–E8)将在阶段一 PR 落地后按表开立;E1/E7 属 spec 车道,届时以
pm:queue转回主队列 spec PM。
17.x 声明式 ApiEndpoint 执行器设计(#5040)
设计基线:
origin/main@70c0769(2026-08-04)。本文按 阶段一落地后的世界 设计:v17 已对非空apis:publish/validate 硬拒(#4936 裁决),handleApiEndpoint死分支、/__api-endpoint遗留前缀、ApiRegistry 全家(#4939)均已摘除。ApiEndpointSchema(packages/spec/src/api/endpoint.zod.ts)词表冻结,本设计纯消费;凡本文标记 [转回 spec 车道] 的条目,一律经pm:queue转交主队列 spec PM,不在执行器车道直做。裁决继承(⛔ 不重议):#4910-Q1=B(
server:seam)、Q3=C(principal 优先 key +trustProxy门控)、Q4=B(kernel-cache 共享计数);#4936 2026-08-04 裁决(响亮拒绝 → 建成后翻转,词表零折腾)。全文两条固定评估轴:长远合理性(项目长期健康)与 防 AI 写元数据犯错(声明即执行,写错在最早的关口被打回,不存在「解析通过然后什么也不发生」的中间态)。
0. 词表与现有管线盘点(设计输入)
冻结词表(
endpoint.zod.ts,defaults 由 Zod 物化):键 形状 本设计的消费点 name^[a-z_][a-z0-9_]*$匹配索引主键、限流桶命名空间、缓存键、OpenAPI operationId path/method^\/绝对路径 /HttpMethodmatchEndpoint匹配维度typeflow | script | object_operation | proxy17.x 只执行 object_operation+flow;script/proxy维持 publish 拒绝(见 §3.4、§5)target/objectParams目标 ID / {object, operation}执行目标委派(§4) authRequireddefault true匿名拒绝门(§3.1) rateLimitRateLimitConfigSchema(enabled/windowMs/maxRequests)复用 #5006 seam(§3.2) cacheTtl秒 响应缓存(§3.3) inputMapping/outputMapping{source, target, transform?}[]纯映射模块; transform维持拒绝(§3.4)summary/description文档 /openapi.json(§3.5)现有可复用管线(全部实测于 origin/main):
dispatcher.dispatch()(packages/runtime/src/http-dispatcher.ts:1471)每请求已完成:kernel 解析(多租户 swap)→executionContext身份解析 → ADR-0069 auth-gate → 项目成员 403 门 → domain registry(ADR-0076 D11)→ 兜底routeNotFound语义 404。执行器要做的只是在 domain registry 未命中与routeNotFound之间插入「端点匹配」一步——正是阶段一摘除handleApiEndpoint后空出来的那个槽位。callData(packages/runtime/src/action-execution.ts:95):protocol 优先(校验+RLS+审计),且自带 ADR-0049apiEnabled/apiMethods暴露门。buildAutomationContext(packages/runtime/src/domains/automation.ts):flow 触发的规范上下文构造,含完整身份信封转发(runAs:'user'fail-closed,The #1888 user-less fail-open is wider than the lint that guards it — record-change flows fired by a system write run UNSCOPED, unlinted #3760/[P0][security] Flow runAs never switches execution identity #1888)。- feat(spec,runtime,hono)!: 入站 rateLimit 从零建 seam —— 授权预算真正产生 429 (#4910, #4937) #5006 限流原语(
packages/runtime/src/security/inbound-rate-limit.ts):deriveBucketConfig/resolveRateLimitKey(Q3=C)/SharedTokenBucketLimiter+createLazyCounterStore(Q4=B)。 MetadataManager的 watcher/集群失效链(metadata-manager.ts:1747 notifyWatchers+metadata.changed集群通道):缓存失效现成。- 错误包络:
buildApiError(runtime/src/error-envelope.ts),error-envelope.conformance.test.ts以源码扫描强制新 domain 文件合规。
1. 挂载:声明路径如何进入
dispatch()#4936 断链之一:dispatcher-plugin 逐条显式注册路由,声明路径落进 Hono 裸
notFound({"error":"Not found"}),连 dispatcher 的语义 404 都到不了。选项
选项 机制 长远合理性 防 AI 犯错 A. 通配 catch-all 路由 dispatcher-plugin start()末尾注册${prefix}/*五个动词的通配路由 →dispatch()⚠️ Hono 同路径多 route handler 是先注册者赢,通配是否遮蔽 REST 的/data静态路由取决于插件 start 顺序——这正是 ADR-0076 D11/OQ#9 点名过的 first-registration-wins 病灶,只能靠顺序约定+一致性测试压住✅ 无中间态 B. publish/装载时逐条注册真实路由 元数据发布时向 IHttpServer注册具体路由❌ 三重结构性错位:① 挂载是 host 全局的而元数据是 per-environment 的(dispatcher-plugin 自己的注释就写明「mounts are host-global while the … service lives in the per-project kernel, so presence is a per-request question」);② 多节点部署要跨实例同步路由表,等于用 HTTP 路由表复制一份元数据状态,两处真相;③ 运行中向 Hono 增删路由的行为(matcher 冻结)未经本仓验证 ⚠️ 发布成功≠每个节点都挂上了,失败静默C. 传输层 notFound 兜底 seam(推荐) IHttpServer增可选setFallbackHandler(handler)(Hono 侧落在app.notFound);dispatcher-plugin 注册兜底器:对${prefix}/下未匹配请求走dispatch()(其中含端点匹配步),其余维持原 notFound✅ 结构上不可能遮蔽任何显式路由——兜底器只在全部路由未命中后运行,零顺序依赖,ADR-0076「一条路由一个属主」以构造保证而非约定保证;顺带把 /api/v1/*的裸 404 升级为 dispatcher 语义ROUTE_NOT_FOUND包络(#4936 记录的裸/语义 404 分裂就此收口)✅ 未声明路径得到带 hint 的语义 404,AI 调试时能分清「路由不存在」与「端点没执行」 推荐 C,A 为备选(若维护者不愿为此扩
IHttpServer契约——该契约在packages/spec/src/contracts/http-server.ts,故 C 的契约面是 [转回 spec 车道] 条目;适配器实现在plugin-hono-server不受限)。中间件 seam(use())不可用:其契约明确req.body不填充(adapter.ts:362),而 flow/create 端点必须读 body。冲突与优先级规则(对 ADR-0076 的回答)
- 内建永远赢,且在 publish 期就赢:声明路径的首段(
${prefix}后第一段)落入保留族即 publish 硬拒。保留族 = domain registry 全部前缀(/health /ready /analytics /i18n /notifications /security /keys /ui /share-links /packages /automation /auth /ai /actions /mcp /meta /data,见http-dispatcher.ts:296起)∪LEGACY_CHAIN_PREFIXES(route-ledger.ts:92)∪ REST 专属族(/batch/search等)∪/environments/projects(scoping 前缀)。拒绝信息带处方(「该前缀由内建<domain>拥有,换一个业务前缀,如/api/v1/<app>/…」)。防 AI 轴上这是最重要的一条:AI 最常犯的错就是把端点声明在/api/v1/data/...下然后困惑为什么行为不对——现在它在defineStack就被打回。 - 运行期兜底器永不竞争:方案 C 下它只见没人要的请求,冲突在构造上不存在;即便走备选 A,匹配器里也只有过了 publish 门的路径。
- 声明路径互撞(同 stack 内同
method+path两条):publish 期硬拒(§5 的 build 门);绕过 publish 直接metadata.register()写入的互撞:匹配器装载期按name字典序保留第一条、error 级日志点名被弃者(确定性 + 响亮,不掷硬币)。 - 保留族列表的双端一致性:spec 侧 build 门无法 import runtime,故列表在 spec 侧 pin 一份常量,runtime 侧新增一致性测试断言「pin 列表 ⊇ 实际 registry 前缀 ∪ ledger」,漂移即红。该测试落 runtime,pin 常量落 spec [转回 spec 车道]。
route-ledger.ts增一行兜底器条目(disposition: 'dynamic'),route-ledger.conformance.test.ts同步——阶段一删掉的/__api-endpoint行不复活,新行以真实机制命名。
边界约束(publish 门的一部分):声明路径必须以运行前缀开头(默认
/api/v1)。兜底器只接管前缀之下;前缀之外的声明路径 publish 拒绝——否则又造一批「声明了但永远不匹配」的死路径。前缀非默认值的部署见 §7 未决项。
2.
matchEndpoint契约归属
选项 长远合理性 防 AI 犯错 A. MetadataManager实现,匹配器为包内纯模块(推荐)✅ 与 #5040 卡面一致(matchEndpoint 落 packages/metadata*);per-environment 作用域免费获得——dispatcher 本来就resolveService('metadata', environmentId)拿到的是 per-kernel 实例;失效直接挂在 manager 自己的notifyWatchers/集群通道上,不新增跨服务事件✅ 元数据的解析、默认值物化、失效、匹配同源,不存在「注册表看见了、匹配器还没看见」的窗口语义 B. 独立 api-endpoint-router服务⚠️ 组合面+1(#4939 刚退役过一个「从未被装配」的独立注册表服务,教训在案);需要自订失效订阅与 env 作用域协议⚠️ 服务缺席时端点静默 404,多一个可缺席件推荐 A。 契约声明在
IMetadataService上(可选方法,与watch?/generateOpenApi?同风格)——packages/spec/src/contracts/metadata-service.ts属 spec,[转回 spec 车道];runtime 消费端沿用typeof === 'function'探测,契约未合入前不阻塞 runtime 开发。签名与语义
/** IMetadataService(可选成员) */ matchEndpoint?(query: { path: string; method: string }): Promise<ApiEndpointMatch | undefined>; interface ApiEndpointMatch { /** ApiEndpointSchema.parse 后的形状 —— 默认值已物化(authRequired 缺省即 true) */ endpoint: ApiEndpoint; /** 路径参数。17.x 恒为 {}(v1 不支持模板段,见 §7);预留槽位使未来加模板不破契约 */ params: Record<string, string>; }
- 匹配维度:
method大写规整后精确比较;path去尾斜杠后整串精确匹配(不做百分号解码、不做大小写折叠——见 §7 未决)。17.x 不发明模板语法:冻结词表对:param/{param}均无定义,发明即造「跑在实现里的隐藏方言」。get/update/delete的记录 id 一律取query.id(阶段一前死代码即此语义,showcase 两端点均为静态路径,零损失)。 - 索引与缓存:首次调用惰性构建
Map<'METHOD path', entry>,数据源list('api');每条经ApiEndpointSchema.safeParse,解析失败的存量条目跳过并 error 日志点名(防 AI 轴:坏条目响亮缺席,绝不半执行)。返回的是 parse 后对象,执行器永远不读原始存储 JSON。 - 失效:manager 内部挂自身
notifyWatchers('api', …);集群写入本就经metadata.changed通道汇入同一函数(metadata-manager.ts:1748),多节点失效免费。失效动作 = 置脏整索引(端点量级为个位数到百位,重建成本可忽略,不做逐条增量)。 - 多租户:无 env 参数——实例即作用域,与仓内其余 metadata 消费完全同构。
3. 逐键接线与激活顺序
总原则(declared = enforced 的落法):v17 硬拒把整面清了场——没有任何存量栈能带着非空
apis:活到 17.x,所以不存在「逐键渐进激活」的兼容问题;所有键在翻转前整体不可达(publish 仍拒),翻转后整体生效。绝不出现「某键已宣传支持、runtime 未交付」的中间态:凡 17.x 不执行的子集(script、proxy、mappingtransform),publish 继续拒绝该子集而不是解析后忽略(§5)。请求期管线顺序(在兜底器→
dispatch()→端点步内,executionContext此时已由 dispatch 解析完毕):matchEndpoint 命中 → ① rateLimit(429 + Retry-After) → ② authRequired(401) → ③ cacheTtl 命中查(仅 GET) → ④ inputMapping → ⑤ 目标执行(object_operation / flow) → ⑥ outputMapping → ⑦ cacheTtl 回填 → 统一包络出口①先于②:限流不区分 401 与 200 消耗,凭据爆破恰是匿名流量,先②会让扫号者绕开计量。③在②后:缓存永远不对未过认证门的请求应答(且键内含 principal,双保险)。
3.1
authRequired(⚠️ 安全语义,默认 true)复用 dispatch 已解析的
executionContext+shouldDenyAnonymous({userId, isSystem})(@objectstack/core,/meta域同款,#3963 起无条件)。authRequired !== false且匿名 → 401ANONYMOUS_DENY包络。默认值由 schema parse 在匹配器内物化,执行器读不到「缺省」这个状态——AI 忘写authRequired得到的是安全默认,写authRequired: false是唯一的开门方式,且 diff 里可 review(与mountRouteOnServer的route.auth !== false语义对齐)。不新建任何 auth seam。3.2
rateLimit(裁决继承:Q3=C、Q4=B)复用 #5006 原语,不新写限流器:
deriveBucketConfig(endpoint.rateLimit)(含 0/负值 boot 期拒绝)→ 每端点一个SharedTokenBucketLimiter(按endpoint.name缓存,匹配器失效时同步失效)→ key =apiep:{endpoint.name}:+resolveRateLimitKey(...)(principal 优先、匿名回落 IP、转发头仅在server.trustProxy下取信——trustProxy从 dispatcher-plugin 既有config.rateLimit.trustProxy透传,端点级与 server 级读同一份声明,不造第二个信任开关)。计数走createLazyCounterStore(kernel-cache 共享,降级一次性 warn 带后果)。命名空间前缀保证端点桶与 server 桶互不侵蚀;server 级中间件在前、端点级在后,两层预算按声明各自独立生效(server 级是全局底线,端点级是业务配额)。429 经buildApiError+Retry-After,与 #5006 完全同构。3.3
cacheTtl词表只说「Response cache TTL in seconds」,执行语义欠定,本设计定为(标注给维护者 review):
- 位置:kernel
cache服务(ICacheService.set(key, value, ttlSeconds))——与 Q4=B 同一存储哲学,多节点命中一致;无 cache 服务时不缓存并一次性 warn(降级响亮)。 - 范围:仅
method: GET端点缓存;其余方法上写了cacheTtl→ publish 拒绝(处方:「cacheTtl 仅对 GET 端点有效」)而非静默忽略 [转回 spec 车道,随 §5 翻转 PR]。 - 键:
apiepc:{env}:{endpoint.name}:{principalId|anon}:{规范化 query}。principal 必在键内——authRequired端点的响应是 RLS 裁剪过的,跨主体共享缓存即数据泄漏;这条是安全规则不是调优项。 - 客户端侧:附
Cache-Control: private, max-age=<ttl>;命中回X-Cache: hit(验收可观测,§5)。 - 失效:端点元数据重发布 → 匹配器失效钩子顺带删该端点键空间(TTL 本身兜底数据变更——
cacheTtl语义即「容忍最多 ttl 秒陈旧」,不做按对象写失效,那是它没承诺的)。
3.4
inputMapping/outputMapping- 纯模块
api-mapping.ts(runtime):source/target按点路径 get/set;inputMapping作用于{...query, ...body}→ 目标入参;outputMapping作用于目标结果 → 响应体;无 mapping 则原样直通(与今日/data、/automation行为一致)。 transform维持 publish 拒绝:全仓不存在「transformation function name」注册表,而stack.zod.ts:751已有同名先例——mappingtransform因无服务端沙箱必须 build 期失败(framework#2611)。解析后忽略 = 教科书式 declared≠enforced。拒绝文案给处方(「用 inputMapping 的字段改名/搬运能力;函数式变换待 transform 注册表立项」)[转回 spec 车道,随翻转 PR]。
3.5
summary/description→/openapi.jsondispatch()今天已探测metaSvc.generateOpenApi(http-dispatcher.ts:1612)——seam 存在,实现缺席(全仓无实现,该路由恒落 404)。推荐:在MetadataManager上实现generateOpenApi(与 matchEndpoint 同源同失效),仅从已装载api条目产出 paths(method/path/summary/description/authRequired→securityRequirement、operationId=name)。契约面(IMetadataService.generateOpenApi?)[转回 spec 车道]。不触碰documentation.zod.ts的ApiDocumentationConfig——那是另一套(疑似死的)词表,本设计不给它续命。
4. 执行目标与错误包络
统一原则:声明式端点 = 既有执行管线的「稳定 URL 别名 + 策略层」,不是第二套执行方言。 同一操作经声明端点与经内建路由的答案必须一致——长远轴上避免双管线漂移,防 AI 轴上意味着端点语义可以从既有文档直接推知。
type: object_operation委派
action-execution.callData(deps, <op>, {object, ...}, undefined, environmentId, executionContext):- 操作映射:
find→'query'(响应success(result.records, {total}),与 specFindDataResponse对齐)、get/create/update/delete同名直达;五个枚举值全部接通(阶段一前死代码只写了三个,那不是契约)。入参:find← query(经 inputMapping);get/delete←query.id;create/update← body(经 inputMapping),update另取query.id。 - 必须传
executionContext——被摘除的死代码调callData('query', {object, query})时丢弃了身份,真跑起来就是以 system 身份绕过 RLS 读数。本设计把这条写成红线并配测试:声明端点的读写与/data同请求同身份逐字节同结果(RLS/FLS/apiEnabled门三者经callData免费继承)。 objectParams缺失或object/operation缺失:publish 拒绝(type: object_operation必须带全)[转回 spec 车道,随翻转 PR]。
type: flow委派
automationService.execute(endpoint.target, buildAutomationContext(mappedBody, context)):buildAutomationContext从domains/automation.ts导出复用(今天是模块内私有函数)——完整身份信封转发,runAs:'user'fail-closed(The #1888 user-less fail-open is wider than the lint that guards it — record-change flows fired by a system write run UNSCOPED, unlinted #3760)、RLS 按触发者([P0][security] Flow runAs never switches execution identity #1888)全部继承;绝不用死代码的runFlow({flowId, inputs})(契约上不存在的方法)。- 服务缺席/stub:
isServiceServeable→capabilityUnavailable(501),与/automation域同款(ADR-0076 D12)。 - 响应:与
POST /automation/:name/trigger完全同构(执行结果经 outputMapping 后success(...))。
错误包络
所有出口经
deps.success/error/errorFromThrown,兜底器侧经errorResponseBase——buildApiError是唯一造错体的地方,error-envelope.conformance.test.ts的源码扫描会强制新 domain 文件合规。thrown 的status/statusCode/ValidationError.fields[]语义(#3867/#3918)自动继承;5xx 消息过looksLikeInternalErrorLeak消毒。
5. 拒绝 → 执行的翻转
选项 评估 环境变量 feature flag ❌ 制造第三态(声明合法+执行与否看部署),flag 会赖着不走;ADR-0049 的敌人 版本门(17.x 起自动) ❌ 拒绝逻辑在 spec 的 publish/validate 里,本无版本开关基建 单一翻转 PR(推荐) ✅ 拒绝在哪里立的就在哪里撤;runtime 各件先行合入但结构性不可达(publish 仍拒,真实组合里零暴露;测试经 metadata.register()直注条目驱动,不走 publish)翻转 PR 的内容(spec 车道,[转回 spec 车道]):把「非空
apis:整体硬拒」收窄为「不支持子集精确硬拒」——type: 'script'、type: 'proxy'、mappingtransform、保留前缀路径、前缀外路径、非 GET 的cacheTtl、缺objectParams的 object_operation、同 stack 内method+path重复;每条拒绝带处方。外加 spec 生成物七路径 regen + changeset。无 D3 semantic migration:v17 已清场,16→17.x 直跳者的apis:从「静默无效」变「真执行(含authRequired默认 401)」,这是能力新增方向的行为变化,upgrade note 一段说明即可,与 #4910 附带确认同理。就绪判据(翻转 PR 的合入门,#4936 实测法,RED-first):
- e2e 第一套件在翻转前的形状上断言拒绝仍在(RED 基线,feat(spec,runtime,hono)!: 入站 rateLimit 从零建 seam —— 授权预算真正产生 429 (#4910, #4937) #5006 范式);
- 真实 boot(
pnpm dev -- --fresh,showcase,同一会话 cookie):GET /api/v1/showcase/tasks→ 200 任务列表,与GET /api/v1/data/showcase_task同身份同结果(阳性对照);- 匿名同路径 → 401
ANONYMOUS_DENY包络(authRequired默认生效的直接证据); POST /api/v1/showcase/inquiries/purge→ 200 且 janitor flow 真实删除了记录(不是 200 空壳);cacheTtl: 30命中:第二次 GET 带X-Cache: hit;- 临时加
rateLimit的端点:超预算 → 429 +Retry-After,双 principal 各自计桶; - 未声明路径
GET /api/v1/showcase/nope→ dispatcher 语义ROUTE_NOT_FOUND包络(不是 Hono 裸{"error":"Not found"});
- showcase 两端点声明恢复(阶段一注释/移除的回迁)+
coverage.ts的apis条目改回真话(引用新执行器机制,不再提handleApiEndpoint); /api/v1/meta/api与/openapi.json均如实反映两端点。
6. 子单拆分(contract-first,一单一落面)
# 内容 车道 主要文件面 Blocked-by E1 契约: IMetadataService.matchEndpoint?+generateOpenApi?+ 契约测试;(若采 §1 方案 C)IHttpServer.setFallbackHandler?[转回 spec 车道] packages/spec/src/contracts/metadata-service.ts(+.test.ts)、packages/spec/src/contracts/http-server.ts(+.test.ts)#4936 阶段一 PR E2 匹配器:纯匹配模块 + 惰性索引 + safeParse 装载门 + watcher/集群失效 metadata(engine) packages/metadata/src/endpoint-matcher.ts(新)、metadata-manager.ts、测试E1 E3 挂载 + 执行步骨架:兜底 seam(hono adapter + dispatcher-plugin)、 dispatch()端点步(匹配→语义 404)、route-ledger 行 + conformanceruntime(cli) packages/plugins/plugin-hono-server/src/adapter.ts、packages/runtime/src/dispatcher-plugin.ts、http-dispatcher.ts、domains/api-endpoint.ts(新)、route-ledger.tsE1、#4936 阶段一 PR( http-dispatcher.ts文件面重叠)E4 策略键: authRequired/ 端点级rateLimit(复用 #5006 原语)/cacheTtlruntime domains/api-endpoint.ts、security/inbound-rate-limit.ts(导出复用)、测试E2、E3 E5 执行目标 + 映射: object_operation五操作经callData(带 executionContext 红线测试)、flow经execute+ 导出buildAutomationContext、api-mapping.tsruntime domains/api-endpoint.ts、domains/automation.ts(导出)、api-mapping.ts(新)、测试E2、E3 E6 generateOpenApi实现(summary/description →/openapi.json)metadata packages/metadata/src/metadata-manager.ts(或伴生模块)、测试E2 E7 翻转 PR:硬拒收窄为不支持子集 + 全部 publish 门(§5 清单)+ 保留前缀 pin 常量 + spec regen + changeset/upgrade note [转回 spec 车道] packages/spec/src/stack.zod.ts、生成物七路径、.changeset/E2–E6 E8 验收:showcase 端点恢复 + coverage.ts修真 + RED-first e2e + 真实 boot 探针(§5 清单)+ 保留前缀双端一致性测试runtime/examples examples/app-showcase/src/system/apis/、coverage.ts、packages/runtimee2eE7 每单量级均在单次 os-dev run 内;E2/E3 可并行(文件面不相交);E4/E5 可并行(同文件不同函数面,若担心冲突则串行 E4→E5)。spec 触碰(E1、E7)一律
pm:queue转回主队列 spec PM,认领时按 #4936 惯例声明完整文件面对账。
7. 风险与未决(不猜,列为 open questions)
代码无法回答、需实现期验证或维护者裁决:
- Hono 路由器的通配-vs-静态优先级与运行中加路由行为未经本仓验证——这是推荐 §1 方案 C(结构性免疫)而非 A 的核心原因;若 C 因契约面被否退回 A,必须先在
@objectstack/http-conformance(ADR-0076 OQ#10 的跨适配器套件)补优先级断言。 - 路径模板:冻结词表未定义
:param/{param}语法。v1 精确匹配 +query.id是不发明语义的最小面;是否补模板语法属词表动作 [转回 spec 车道候选,不假设]。ApiEndpointMatch.params槽位已为此预留。 script/proxy执行语义:IAutomationService.execute注释称「flow or script」,但 script 目标是否真的经execute可达未验证;proxy则是全新出网面(SSRF/出口策略),安全语义必须单独立卡裁决。17.x 两者维持 publish 拒绝(带处方)——这是 declared=enforced 的正确姿势,不是欠账。cacheTtl的精确语义(服务端缓存 vs 纯 header、principal 键、GET-only)为本设计所定,词表未定;§3.3 的安全规则(principal 入键)请维护者确认为红线。- 匹配的字符层语义:百分号编码、Unicode 规整、大小写——v1 按原串精确比较;是否需要 RFC 3986 规整化留观察(写进 matchEndpoint 的 describe/文档)。
- draft vs published:
MetadataManager有publishedDefinition快照(ADR-0033),但list('api')返回的是工作定义还是发布定义、其他执行面(flow)如何取舍,未从代码验证;匹配器应与 flow 装载采同一策略,实现期对齐并写测试钉住。 composeStacks静默丢弃非数组顶层键(composeStackssilently drops every non-array top-level key —api:today,server:as of #4910 #5005):apis:是数组键应不受累,但该单的合并语义裁决未落,组合栈路径的端点装载在 E8 验收中必须显式覆盖。- 前缀非
/api/v1的部署:声明路径含前缀字面量(showcase 即/api/v1/...),部署改config.prefix后声明与运行前缀脱钩。v1 以 publish 门(「路径必须落在运行前缀下」)暴露问题;是否引入前缀相对声明属词表动作 [转回 spec 车道候选]。 - scoped URL 变体(
/environments/:environmentId/...):声明端点是否需要 scoped 别名(如 automation/AI 路由那样双挂)未定;v1 不提供,多租户经 host 头/kernel 解析已可达,显式 scoped 别名留观察。 - 词表变更需求:目前为零 ——本设计全程未发现必须改
ApiEndpointSchema的点;上列 2/8 两条为「若产品要」的候选,均不预支。
Generated by Claude Code
- §1 推荐 C(
维护者三项裁决(2026-08-04)—— 设计文档否决窗口关闭,方向落定
- 命名空间制:声明式端点路径收紧为
/api/v1/apps/<应用名>/<子路径>,apps为唯一平台保留切出段,应用段派生自 stack 身份。设计 §1 的保留前缀 pin 清单 + 双端一致性测试整体作废(由命名空间规则替代),§7-8(前缀脱钩)大幅收窄; - 通道分工:actions = 平台内命令(会话调用方),apis = 平台外集成面(资源 + webhook)。判据写进两侧 schema describe(spec 车道执行项);
type: flow保留,三条纪律:分工判据入 describe、同管线红线(§4 已立)、E7 新增 publish 门:authRequired: false必须伴随rateLimit,否则拒绝附处方;签名验证列为将来词表候选,不预支。
裁决全文与依据(含主流平台调研)见 ADR 起草单 #5060(已派发,docs-only PR,预计 ADR-0120);ADR 落地不晚于 E 系列开派。对子单表的影响:E7 的 publish 门清单更新为 —— 命名空间规则(替代保留前缀族)、匿名须限流、不支持子集拒绝(script/proxy/transform/非 GET cacheTtl/缺 objectParams/同栈 method+path 重复);E1/E3 因保留族一致性测试作废而略减面。§1 挂载机制(方案 C vs A)与 §3.3 cacheTtl 语义两处否决窗口维护者未提出异议,按设计推荐执行。
Generated by Claude Code
- 命名空间制:声明式端点路径收紧为
ADR-0121 已合入
main(PR #5064,经合并队列落地;#5060 闭环)。E 系列开派的 ADR 前置就此满足 —— 剩余前置仅 #4936 阶段一 PR(在飞)。执行口径固化为:E7 落 D1/D2 命名空间 publish 门(Q1 采 A:声明apis:的 stack 必须显式manifest.namespace,无派生回落)+ D6 匿名须限流 + 不支持子集精确拒绝;D3 判据入两侧 describe(spec 车道执行项);showcase 端点按/api/v1/apps/showcase/…形状随 E8 回迁。
Generated by Claude Code
- added a commit that references this issue
on Aug 4, 2026 阶段一 PR #5065 已验收(全绿),入队中。两条 E 系列执行注记:
- E7:v17 拒绝文案里「your endpoint definitions stay valid」在 ADR-0121 命名空间制下需修正 —— 翻转 PR 重写该文案时,把「path 须迁移为
/api/v1/apps/<namespace>/<子路径>形状」写进处方(showcase 回迁同理,E8); - E6:
GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 实测发现GET /openapi.json已由packages/rest/rest-server.ts:2523真实拥有(dispatcher 侧generateOpenApi探测分支全仓零实现,恒 fall-through)。设计 §3.5 在 MetadataManager 上实现generateOpenApi之前,必须先裁「一路一主」:要么 openapi 面走 rest 侧现有属主(dispatcher 死分支删除),要么属主迁移 —— 不得造成双属主并存。GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 已标domain:cli,建议与 E6 合并处置。
Generated by Claude Code
- E7:v17 拒绝文案里「your endpoint definitions stay valid」在 ADR-0121 命名空间制下需修正 —— 翻转 PR 重写该文案时,把「path 须迁移为
里程碑:阶段一已落地
main(29c6c9d,PR #5065),落地后复核通过 —— 退役条目零残留(ApiEndpointRegistrationSchema家族在 api-surface / authorable-surface / manifest 全部消失)、幸存者完好(ApiEndpointSchema、ConflictResolutionStrategy)、硬拒带 #5040 指针在 main、兄弟单 #5006 实现体无损。#4936 / #4939 已关闭并完成标签卫生。E 系列状态:维护者裁定 v17 切版后开工(E7/E8 结构上必须在拒绝随 v17 发出之后;E1–E6 不挤 rc 窗口)。经批准,E1 已提前立单占位:#5080(spec 车道队列,
Blocked-by: v17 切版,认领前核.changeset/pre.json)。E2/E3Blocked-by:#5080,切版信号到达后由本车道按表开派。
Generated by Claude Code
E6 设计修正(基于 #5078 实测证据,替代设计 §3.5 的原推荐)
实测已定(全文见 #5078 分诊评论):
GET /api/v1/openapi.json由 rest-server 独占应答(355KB 真文档,Host 注入 / 199 paths 展开 / x-template 三指纹),dispatcher 的generateOpenApi探测分支「双重死」(零实现 + 零到达路由)。据此按 ADR-0076 一路一主修正:- ❌ 原 §3.5:在
MetadataManager上实现generateOpenApi—— 作废。那会造出/openapi.json的第二属主,正是 0076 禁止的形状; - ✅ E6 重定义:endpoint
summary/description/operationId经 rest-server 既有 enrichment 管线进入 OpenAPI 文档(它已按getMetaItems展开对象路径,端点条目走同一根);同 PR 摘除 dispatcher 的generateOpenApi死分支、修正route-ledger.ts:252失准注记(GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 并入 E6 执行); - E1(E1(#5040 执行器契约面):
IMetadataService.matchEndpoint?/generateOpenApi?+IHttpServer.setFallbackHandler?可选契约方法与契约测试 #5080)随之收窄:IMetadataService.generateOpenApi?契约成员剔除(已在该单注记),E1 仅剩matchEndpoint?+IHttpServer.setFallbackHandler?; - 佐证:boot 中 12 条未路由路径全部落 Hono 裸 404、
ROUTE_NOT_FOUND语义包络在 HTTP 上不可达 —— §1-C 兜底 seam 的「裸/语义 404 收口」价值被实测加强。
维护者否决窗口照例(不阻塞,E 系列仍等 v17 切版)。另:boot 顺带发现的
/auth/*500 泄漏已立 #5085(cli 车道,与本程序无关)。
Generated by Claude Code
- ❌ 原 §3.5:在
- addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVP
on Aug 4, 2026 🟢 E 系列开工(维护者放行,2026-08-04:「这个需求挺重要的,可以先开发」)
放行范围:E1–E6 的开发与落地(均结构性不可达,publish 仍拒,零现网行为变更)。E7(翻转)/E8(验收)仍守裁决分期 —— 拒绝必须先随 v17 发出,切版后才派。总表落号:
单 Issue 状态 Blocked-by E1 契约面 #5080 🔵 已派发(branch claude/issue-5080-executor-contracts)—(v17 门已撤,见认领评论) E2 匹配器 #5089 待派 #5080 E3 挂载 seam #5090 待派(与 E2 并行) #5080 E4 策略键 #5091 待派 #5089 + #5090 E5 执行目标 #5092 待派(与 E4 并行) #5089 + #5090 E6 文档面(并入 #5078) #5093 待派 #5092 E7 翻转(spec 车道) 未立 ⛔ 等 v17 切版 v17 发版 + E1–E6 E8 验收(showcase 回迁 + e2e) 未立 ⛔ 等 v17 切版 #5093 + E7 派发节奏:R1 = E1;R2 = E2+E3 并行;R3 = E4+E5 并行;R4 = E6。各单卡上均注明「执行器车道程序卡,其它车道请勿认领」。本车道 PM:
session_01EYGdmvWP1ieZSLqvAW6uyd。
Generated by Claude Code
维护者裁决(2026-08-04,二项)
- 不加显式激活开关 —— 依赖两层既有机制:词表安全默认(
authRequired缺省即true,E1 已物化进契约)+ E7 的 publish 校验门。维护者理由:ObjectStack 应用由 AI 维护,升级文档写明变更,AI 维护者即知 —— 强制审视由升级文档承担,不再由版本通道或开关承担。推论:升级文档从「附带产物」升为「安全承载件」:E7 的 regenupgrade-guide条目 + E8 的手写迁移指引必须显式包含「审视历史apis:配置、尤其显式authRequired: false条目」的指令,验收时按安全件复核。 - 执行器直接进 v17 —— E7(翻转)/E8(验收)解除切版门,立即立卡入链,v17 切版等 E 系列全绿。原「v17 = 仅拒绝」分期废止;拒绝态成为 main 上的中间历史而非发布过的状态。
E7/E8 随后立卡补入总表。E2(#5089)已交付 PR #5110,复核中;E3(#5090)开发中。
Generated by Claude Code
- 不加显式激活开关 —— 依赖两层既有机制:词表安全默认(
总表更新(E7/E8 立卡,v17 目标生效;E8 的 Blocked-by 即 #5111)
单 Issue 状态 E1 契约面 #5080 ✅ MERGED(PR #5097, 77be690,落地复核毕)E2 匹配器 #5089 🔵 PR #5110 已验收、auto-merge 入队(实现落 packages/metadata,按设计表修正卡面的 runtime 笔误;越范围发现 #5108/#5109 已分诊入队)E3 挂载 seam #5090 🔵 开发中(与 E2 并行) E4 策略键 #5091 待派(E2+E3 落地后,R3 与 E5 并行) E5 执行目标 #5092 待派(R3) E6 文档面(并入 #5078) #5093 待派(R4) E7 翻转 #5111 已立卡,Blocked-by E4+E5+E6;发布目标 v17;升级文档按安全承载件交付 E8 验收 #5112 已立卡,Blocked-by #5111;v17 切版等本单绿
Generated by Claude Code
R3 闭环:E4 #5091(PR #5135,
a70cd0a)与 E5 #5092(PR #5136,277eb36)均 MERGED,落地复核零吞并。过程记录:E4 出现一处 NUL 字节缺陷(PM 复核发现并修复,dev 独立收敛,零丢失);两单在 conformance 清单同锚点的预判冲突如期发生,PM 改基保留双条目解决。E5 的映射缺口归并至 #5137(E5c,Blocked-by E5b);#5138(callData 记录不存在三态)观察类持有。E5b(#5129)已派发(branchclaude/issue-5129-wire-endpoint-chain)—— 接线后端点链在真实 boot 上打通(仍受 publish 硬拒保护,结构性不可达)。后续:E5c/#5137 → E6 #5093 → E7 #5111(门校验四要点已挂该单)→ E8 #5112 → v17 切版。
Generated by Claude Code
🏁 E7 翻转落 main(
d21c001),收官双单并行开发中单 Issue 状态 E1–E6 + E5b/E5c — ✅ 全部 MERGED,逐单落地复核零吞并 E7 翻转 #5111 ✅ MERGED(PR #5188):五道门挂 schema superRefine,五条 publish/validate 入径全覆盖;升级文档安全承载件(declarative-apis-endpoints-live)落 registry 经 regen 发布;canonical 路径上移词表单点拥有E7b 门兜底 #5189 🔵 开发中(E7 dev 发现的绕行: publishPackage/直写不过门,D6 无运行时对偶 —— publish 层接门 + 匹配器装载期拒绝,双层)E8 收官验收 #5112 🔵 开发中(showcase 回迁 + RED-first e2e + 真实 boot 探针 + 升级文档安全复核 + 多租户交错;#5168 悬空 $ref不计失败)E7b + E8 双绿 = E 系列完结 = v17 可切(维护者裁决:执行器直接进 v17,切版等全绿)。
Generated by Claude Code
9 remaining items
- added 4 commits that reference this issue
on Aug 4, 2026 E9 补记(#5238,PR #5246,
c142ced):文档面追平完成 ——App.apis墓碑与server模块头的失真半句摘除(历史保留,双侧钉法)、http-protocol.mdx新增 Declarative Endpoints 节、quick-reference.mdx速查条目、objectstack-apiskill 按现状教学(以objectstack validate处方为真源)。文档断言全部实测(defineStack 三探针)。衍生:#5244(→#5231 子单)、#5245(skill compatibility 行,finding留维护者裁写法)。至此 #5040 程序全部十二单(E1–E9 + E5b/E5c/E7b)完结,文档/skill/例子三层齐平,v17 可切。执行器车道按维护者指令暂停。
Generated by Claude Code
关闭(维护者指示,2026-08-04)
spec 车道 PM(
session_01ErbEDVAg1No9gdg1pgDAGB)受维护者之问复核本卡,代已暂停的执行器车道收口。核验(不取评论自证,取 GitHub/git 实据):- 十二单(E1–E9 + E5b/E5c/E7b)对应 PR 全部 MERGED;抽验 E7
d21c001、E89f41ee6、E9c142ced均为origin/main祖先; - 切版门 E8(#5040 执行器):验收 —— showcase 端点回迁
/apps/showcase/…+ RED-first e2e + 真实 boot 探针 + 升级文档安全复核 #5112(E8,「v17 切版等本单绿」)已 completed 关闭,E9(#5040 收尾):文档与 skill 追平执行器 —— 摘除「apis 被拒」失真表述,objectstack-api skill 纳入端点能力 #5238(E9)同; - 卡面完成判据(硬拒→执行翻转、
ApiEndpointSchema词表零折腾)已由 E7/feat(spec)!: 声明式 apis: 翻转 —— 硬拒收窄为逐端点门(#5040 E7) #5188 + E8/test(e2e,showcase): 执行器收官验收 —— showcase 端点回迁 + 真实 boot 探针(#5040 E8) #5230 兑现并实测验收; - 衍生开放项(
api不在 metadata 类型注册表里 —— Studio 直写路径完全不校验端点,publishPackageDrafts 也没有 E7 门 #5206/声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224/showcase 的showcase_inquiry_purge清扫流从来没删掉过任何记录 —— delete_record 节点每次都失败(Delete requires an ID or options.multi=true) #5225/objectstack dev --fresh上 sys_audit_log 表不存在 —— 每次元数据写入都以 ERROR + 「Audit write failed」告终,审计轨迹静默丢失 #5226/TS 作者写不出「省略 authRequired」这个安全形状:ApiEndpoint是输出类型,authRequired在上面是必填 #5227/E 系列收尾:摘除已失真的「structurally unreachable」注释 + 多租户 decline 分支补零覆盖 #5231/showcase 端点注释把cacheTtl的响应头写成public, max-age=30—— 运行时发的是private,而private是安全规则 #5244/十份 SKILL.md 的compatibility仍写「Requires @objectstack/spec 16.x」—— 而它们教的已经是 17 的能力 #5245)均已独立立单入各自队列,不悬于本卡,关闭零丢失。
v17 切版就绪状态不变,由切版清单(终局重测 +
changeset pre exit)跟进,不再以本卡承载。
Generated by Claude Code
- 十二单(E1–E9 + E5b/E5c/E7b)对应 PR 全部 MERGED;抽验 E7
- added a commit that references this issue
on Aug 4, 2026 - added 4 commits that reference this issue
on Aug 6, 2026 - added a commit that references this issue
on Sep 28, 2026
维护者已于 #4936(2026-08-04 00:20Z 裁决)批准立项,本卡为裁决第 2 条的落项位,不在 v17 窗口内执行:
Blocked-by: #4936
范围(自 #4936 裁决与实测证据继承)
apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 实测两处断链之一)。执行器需要给元数据声明的端点一条真实进入dispatcher.dispatch()的通路。matchEndpoint:在元数据服务上真实实现(声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 时全仓无实现,handleApiEndpoint恒{ handled: false },该死分支已随 声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 的 PR 摘除,本卡是重建而非复活旧代码)。authRequired(true生效即 breaking 面)、cacheTtl、inputMapping/outputMapping;rateLimit两键随 「v17」入站 rateLimit 接执行:ApiEndpoint / HttpServer 的 RateLimitConfig 推导为 runtime token bucket 配置,dispatcher 生效(#4686 拆向之一) #4910-Q2 裁决在此接线(复用 feat(spec,runtime,hono)!: 入站 rateLimit 从零建 seam —— 授权预算真正产生 429 (#4910, #4937) #5006 建的 server 级 seam 范式:principal 优先 key、kernel-cache 计数)。apis:publish/validate 硬拒」(声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936)换回执行;ApiEndpointSchema词表零折腾(裁决依据:端点词表是行业极稳定形状)。分诊注记
落点跨域:挂载在
packages/runtime(按域表归domain:cli,取主落点标之)、matchEndpoint在packages/metadata*(domain:engine)、词表在packages/spec(domain:spec)。被取用时按 contract-first 拆分(spec 子单先行),不整卡直派。17.x 窗口打开、#4936 的 PR 合入之前,本卡不入pm:queue。关联:#4936(裁决全文)、#4939(ApiRegistry 同批退役,词表收敛为一套)、#4910 / PR #5006(server 级 rateLimit seam 范式)、ADR-0049、ADR-0076。