Repository navigation
getEffectiveLock 的 overlay 读用裸 catch,sys_metadata 读失败时保护闸门 fail-open(_lock 落成 'none',写/删被放行) #5706
Description
Activity
分诊:入队
pm:queue+ 挂pm:blocked(Blocked-by: #5532已补正文),域domain:engine-core。- 落点锚定:
packages/metadata-protocol/src/protocol.ts的getEffectiveLockoverlay 读 →metadata*家族 →domain:engine-core。 - 阻塞理由:建议修法复用 PR fix(metadata-protocol): 元数据存储读不到不再被讲成「这一项不存在」 (#5532) #5705(getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532)引入的
rethrowUnlessMetadataStoreUnprovisioned,且与该 PR 同文件在飞(其 base 即今日 main HEAD)—— fix(metadata-protocol): 元数据存储读不到不再被讲成「这一项不存在」 (#5532) #5705 落地前派发必撞;落地后 unlock sweep 回队。 - 定级理由:保护闸门 fail-open(ADR-0010 §3.3 锁判据在 sys_metadata 读故障时把
_lock讲成'none',写/删被放行)是 restore-invariant 向的具体缺陷,落点与修法齐备,不需要决策箱;wire 可见变化(写路径在读故障时 503)按正文要求进 changeset,派发令应点名。 - 查重:同族三单三落点已互链 —— getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532(读侧,在飞)、getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」 #5707(layered 读,观察类持有)、本单(写闸门);消费方与后果类别各不相同,不并单。
- 过时前提检查:origin/main@
eb26126上protocol.ts最近改动为 feat(spec,runtime,metadata-protocol)!: discovery 两个生产者统一到一个 schema —— capabilities 正名、features/endpoints 退役、scoping 声明 (#4828) #5682,正是本单实测基线,前提新鲜。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- 落点锚定:
解锁 + 认领:PM 循环第 3 轮(engine-core 车道)。Blocker #5532 已随 PR #5705 合并关闭,按 unlock sweep 摘
pm:blocked回队并立即认领(fail-open 保护闸门优先占protocol.ts空位,#5264 排下一位)。会话:
session_01V7WetGmnfoXNn8cLieKKmx
分支:claude/issue-5706-lock-gate-fail-closed
Worktree:objectstack-issue-5706
域:domain:engine-core
文件面:packages/metadata-protocol/src/protocol.ts(getEffectiveLock 的 overlay 读一处 catch)、metadata-protocol 测试、.changeset/*.md(写明 wire 可见变化:save/publish/rollback/delete 在元数据存储读故障时 503,而非当作没锁放行)。
修法按 issue 建议:复用 #5705 刚落的rethrowUnlessMetadataStoreUnprovisioned(未建表良性 →'none'是真相;其余照实抛 503)。「拒绝一次不确定的写,好过放行一次本该被拒的写」。
Generated by Claude Code
复核通过,ACCEPT(engine-core 车道 PM,第 3 轮):PR #5736,CI 绿后转 ready 入队。
交付精确到裁定范围:一处 catch + JSDoc,复用 #5705 的判别,闸门判定逻辑零改动(503 自然上抛有测试钉住);窗口建模修正到位(「读失败但写成功」—— dev 的第一版 harness 因全挂掩盖闸门,自行发现并修正);修复前 fail-open 实测复现(
success:true且写真执行、审计只有allowed行 —— 缺陷长期不可见的原因被一并解释);反向验证 5 红且如实说明与预测 4 的偏差,7 绿各有承重理由(含「真 miss 照常放行」防「什么都拒假装 fail-closed」)。issue 未验证点全枚举:getMetaItem 路径不同病(已被 #5705 覆盖),layered 路径同形但归 #5707 持有(含其未定设计选择),零重复立单。wire 变化(读故障时写路径 503)已进 changeset。本单从 #5705 合并解锁到交付共 ~35 分钟,是本车道当日最快闭环。#5264 排 protocol.ts 下一位。
Generated by Claude Code
- added a commit that references this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 17, 2026
Blocked-by: #5532(同文件同族在飞:PR #5705 引入本单建议复用的
rethrowUnlessMetadataStoreUnprovisioned;其落地后 sweep 回队)。做 #5532(getMetaItem overlay 读把 outage 吞成 miss)时,在同一个文件里发现的同族点。不在那单范围内(#5532 / PR #5705 的文件面被限定为
getMetaItems/getMetaItem的四处 overlay 读 +getMetaItemCached终末错误),按 Prime Directive #10 单独记在这里,unassigned。与 #5532 的关系:同一条裸 catch 家族、同一个文件,但消费方不同、后果类别不同 —— #5532 的后果是「读」被讲错(可用性故障被讲成不存在),这条的后果是「保护闸门失效」,写路径被放行。
位置
packages/metadata-protocol/src/protocol.ts,getEffectiveLock(私有,以内容定位:注释// 2. Overlay row.之后的try):为什么是缺陷
getEffectiveLock是 ADR-0010 §3.3 锁闸门的唯一判据来源,两个调用点都是写路径的准入:assertLockAllowsWrite(save/publish/rollback)assertLockAllowsDelete两者都是
const state = await this.getEffectiveLock(...)然后evaluateLockForWrite(state.lock)。lock: 'none'意味着「没锁」→refusal为 null → 返回 null,写放行。于是:overlay 行里声明的
_lock在sys_metadata读失败时静默变成「没有锁」,一次被拒绝的写会变成一次被允许的写。这是 ADR-0049 的 fail-closed 方向反过来 —— 一个「读不到」被当成「作者没声明保护」,正是 ADR-0110 D3 点名的那个禁止推论,只是这一次落在安全判定上而不是展示上。审计侧也一并失真:放行路径不会写
outcome: 'denied'的审计行,所以事后也看不出这次写本该被拒。缓解与真实窗口(如实记录,不夸大)
getEffectiveLock先查lookupArtifactItem(纯内存 registry),打包件声明的_lock仍然生效。失效的只有 overlay 来源 的锁(lockSource: 'overlay')。this.environmentId === undefined(control-plane)时两个 assert 直接 return null,不走这里。即便如此,「保护闸门在读失败时默认放行」本身就是不该存在的形状,不依赖窗口大小。
建议修法(与 #5532 / PR #5705 对齐,不代裁决)
PR #5705 已经在同文件引入了
rethrowUnlessMetadataStoreUnprovisioned:按错误类型判别,isMissingTableError(表还没建 → 确实没有 overlay 行 →'none'是真相)良性放行,其余抛status: 503/code: SERVICE_UNAVAILABLE,驱动错误挂cause。这里直接复用同一个私有方法即可,一处 catch 的改动。需要注意的差异:这条路径的抛出会让
saveMetaItem/deleteMetaItem在元数据库读故障时以 503 失败,而不是「当作没锁然后去写」。这是期望的方向(拒绝一次不确定的写,好过放行一次本该被拒的写),但属于 wire 可见变化,应在 changeset 写明。未验证:是否还有其它调用点通过别的路径读同一行锁状态(
resolveLockState走的是已取到的 item,不经过这里)。关联
#5532 / PR #5705(同文件同家族,读侧)、ADR-0010 §3.3(锁语义)、ADR-0049(declare-and-enforce / fail-closed)、ADR-0110 D3(miss ≠ outage)、#5108(
DatabaseLoader复数读,先例)。Generated by Claude Code