Repository navigation
[REQ-128][CODE] 实现 secret prerequisite 并消除 renderer 真值回显 #696
Description
⚠️ 本期范围裁决(2026-07-30,owner)—— 优先于下方原文与基线REQ-128 分四相,本期只做 Phase 1,只支持单组件 package(无 Bundle)。裁决见
alpha-work#49。
本票是 Phase 1 唯一的认证面(#703MCP OAuth /#704Alpha Connection 均已移入 Phase 2)。一、
alpha-code#694已交付的 host 契约面(本票消费它)
- profile =
skill/agent/mcp-local/mcp-remote(无cloud);mcp-remote的auth枚举不含oauth/alpha-connection;- capability 词表只有
alpha.secret-prerequisite.v1—— 即本票要兑现的那一个。
词表在 decoder 里就是白名单:在册即宿主声称支持。所以本票不落地,这个 token
就是超卖。这是本票与契约票之间唯一的活契约。- Development plan 第 6 条(为
#712输出 prepared-resource API)随#712移入 Phase 2,
本期不做,也不要为它预留接口。二、
liveMcp回显删除:先勘破,不要发明这是本票的真正风险点。现状(已勘破,别重复验证):
ext-install-planner.ts:262定义liveMcp,:2093与:3565返回;preload/types.ts:698/728明文承认 "post-policy config + secret plaintext";renderer/extensions/use-extensions.ts:355-359调liveAddAndConnect()→c.mcp.add→c.mcp.connect;- 也就是说今天调引擎
/mcp/:name/connect的是 renderer,不是 main
(ext-session-grants.ts文件头注释写明了这条信任边界)。因此"写盘后由 main/engine reload 或 reconnect"这句话背后可能没有现成通道。
硬要求:动笔前先枚举 main 侧今天有没有可复用的、已认证的 main→engine typed route。
- 有 → 复用它,在 PR 描述里点名是哪一条。
- 没有 → 停下来,把勘破结论写回本票,不要自己发明一套 main→engine 通道。
新建跨进程通道是架构决策,不是本票能顺手做的事,编排者会另行裁决。无论哪种,都不得为了删掉明文回显而让"装完 MCP 立刻可用"这个用户可观察行为退化成
"要重启才生效"却不说 —— 若确实退化,必须在报告里明说,由编排者判。三、反过度工程(owner 直令)
必须在当前框架下开发。既有 secret 通道已完备且不要重造:
alpha-mcp-secrets.ts(0700 目录 / 0600 文件 /v-<hex>append-only 版本目录 /
独占 claim / 10 分钟 GC 宽限)、alpha-secret-files.ts、{file:}引用由引擎
ConfigVariable在装载期解析。本票是收口,不是新建 secret 子系统:
不做 credential manager、不做 rotation 产品面、不切 safeStorage、不引入新依赖、
不为 Phase 2 预留插槽。
Outcome
消费 signed Envelope secret prerequisite,建立main-owned secret reference/store边界并消除
当前 liveMcp 真值回显;MCP OAuth与Alpha Connection已拆到#703/#704。
Covers
alpha-work#49 AC4/AC5 的 secret半场。
Solution baseline
Accepted REQ-128 baseline
§2.6–§2.8。
Ground truth and boundary
preload/types.ts、ext-install-planner.ts、use-extensions.ts当前会把替换后的
liveMcpsecret真值main→renderer→SDK回传;本票必须删除。- Catalog/Envelope只含secret id/label/required/injection target,不含值。
- renderer值只在表单短生命周期内存;main校验declared set并写现有受限store。
Development plan
- strict decode Envelope auth-prerequisite profile;Desktop不decode web Declaration。
- 定义shared prerequisite evaluator中的secret state/reason/reference。
- 删除preload/result
liveMcp真值,写盘后由main/engine reload/reconnect。 - Manifest/receipt/log/telemetry只记录reference/status,不记录value/value digest。
- cancel、replacement、uninstall、missing/stale reference fail closed;完整credential
rotation产品面不在本票。 - 为#712输出prepared-resource所需的typed secret reference API。
Acceptance and named gates
package-secret-prerequisite.test.ts覆盖undeclared/cancel/replacement/uninstall/stale/no-value。- preload/result/renderer global state/log/receipt扫描无secret value。
- renderer tamper target/id无效,main重验signed prerequisite。
- 既有secret MCP单装与rollback零回归。
Dependencies
Blocked by alpha-code#694。
Out of scope
MCP OAuth、Alpha Connection、credential manager UI、safeStorage/engine generation切换。
Exit condition
Merged security PR + negative/wiring tests + canonical secret docs同步。
Activity
[-][REQ-128][CODE] 编排通用 secret、MCP OAuth 与 Alpha Connection prerequisite[/-][+][REQ-128][CODE] 实现 secret prerequisite 并消除 renderer 真值回显[/+]jinjunnn commentedon Jul 30, 2026
R1 跨模型对抗审计记录(2026-07-30)
实现方 codex,审计方 Opus(只读,在 scratchpad 副本里做 mutation,未动实现方工作树)。
预算 M 级 ≤2 轮,本轮 R1。
头号问题的结论:
global.dispose是现有最小杠杆,设计选择正确编排者派审时的怀疑是"把 MCP 安装从不打扰换成了打断活跃流,是不是过度手段"。
审计去读引擎源码求证(非推断),结论一半成立一半不成立:
成立的一半 —— 确实会打断,而且可达。 不是"可能":
POST /global/dispose→disposeAllInstancesAndEmitGlobalDisposed→InstanceStore.disposeAll→ 每个
InstanceState的ScopedCache.invalidate→run-state.ts的 finalizer 对每一个在跑的session runner 调
runner.cancel=Fiber.interrupt;外加mcp/index.ts:531-556对每个 stdio MCP的整个子进程树发 SIGTERM。影响范围是所有已打开项目实例,不只扩展中心那个。
可达性走我们自己的 runbook:首装因
ext-install-policy落installedDisabled:true不触发,但用户手动开启后再重装/更新(
nextDesiredState是 prior 优先)就落enabled→ 触发。审计实测复现。不成立的一半 —— 没有更窄的路。
MCP.connect()不从磁盘重读配置(mcp/index.ts:648-651→requireMcpConfig→:790-798);{file:}只在配置装载期展开(config/config.ts:213-224→ConfigVariable.substitute);Config.get走InstanceState的ScopedCache,只在实例 boot 时装载一次,全仓无文件监听。更窄的
POST /instance/dispose只处理带?directory=的那一个实例,而 hub 客户端不带 directory⇒ 用户项目实例仍看不到新 server。
所以爆炸半径是固有的,不是选择错误。真正的缺陷是它没有上界(M1)。
采纳并回传实现方
withRecoveredWrite的串行 gated-write 链里mcp.add15s /mcp.connect10s / dispose 5s)。GET /mcp在 dispose 后是冷启,每 server 受 30s 约束,8 条 MCP 合法跑到 ~30s,期间 hub 全部 gated 写排队;最坏一档client.close()无 timeout 且Effect.uninterruptible,IPC 可能永不返回。修法=套仓内已有withTimeout,超时返回已存在的reload-pendingext-ipc.ts—— 而 preload result 现在正是在那儿组装mcpActivation接线、往 IPC 结果塞liveMcpPlaintext: intent.grants.secrets。另外not.toContain(secretValue)按构造恒真。这是本票存在理由的那道闸,而它是假的。 修法=在既有ext-write-channels.test.ts里断言返回值(canary 密钥),不动生产结构gate-files.tsv:57下界 11 vs 实跑 14reload-pending正好覆盖新增的 timeout 路径关于 M3 的编排者裁决(重要,影响 Phase 1 的收尾判据)
shared/package-secret-prerequisite.ts的 5 个导出符号在模块与测试之外 0 生产调用者,它消费的
#694decoder 同样还没有生产入口链 —— 因为入口链是#702(evaluator 接线)与#713(admission)的活,这是依赖顺序,不是本票的缺陷。但
docs/contracts/extension-capability-authorization.md的新增段把它写成了当前有效行为。这正是 REQ-127 那个形态:一条没跑过的文档断言成为下一份基线的 ground truth,同时决定选型和闸门。
裁决:改措辞,不改范围。 那段改写为"尚未接线的 package 安装入口的声明形状",
并明写 —— 在
#702与#713落地之前,alpha.secret-prerequisite.v1在运行期尚未兑现。由此产生的 Phase 1 收尾判据(记在这里,
#713收尾时必须核):#713的退出门必须包含一条端到端证据,证明alpha.secret-prerequisite.v1从签名 Envelope一路走到真实的 secret 采集与写盘。在那之前,
#694的词表里声明该 token 属于"已声明、待兑现",不是超卖 —— 但必须有人在 Phase 1 结束时核对它真的兑现了。
同时保留本票确实是活行为的那半场:
liveMcp明文回显已删除(全仓 0 匹配)、main 侧引擎重载已接线 —— 这半场不降级为"声明形状"。
已知不修 + 理由(留痕)
package-secret-prerequisite.ts里 6 处无测试保护的检查(reference 键白名单、SAFE_VERSION、capability↔requiredSecrets 一致性、SAFE_SERVER、requiredSecrets 排序去重、payload/profileId 匹配):261的record.status !== "ready"不可达分支needs_auth/needs_client_registration被映射为failedlocal,经我们自己的路径到不了这两个状态kind:"bundle"同样不返回liveMcp,存量非回归serverReady是进程生命期单一 Deferred,sidecar 换端口重生会发过期 urlGATED_WRITE_CHANNELS把installCatalog声明为(intent: unknown) => Promise<unknown>,导致mcpActivation联合在编译期不受约束reload-pendingmcp.connect读 boot 期配置快照,存量问题,本票既未引入也未加重审计方的正向取证(值得记)
真装一次带密钥的 MCP(local + remote 各一次)逐面打印:outcome 键恰好
kind,manifestDigest,name,ok;tmp树下唯一含 canary 的文件是版本化密钥文件;installs.json/alpha.jsonc/ext-tx/journal/ext-tx/authz/ext-store/*/grants.json全部 clean;
computeGrantDigest只哈希 secret 名字。planner 半场的零回显是真的 ——假的是那道闸门(M2),不是那个行为。
[#696][CODE] secret prerequisite 消费 host Envelope + 删除 renderer 明文回显 (#…
jinjunnn commentedon Jul 30, 2026
R2 审计记录与合并(2026-07-30)
R2 结论:
MERGE-WITH-FIXES—— R1 五条全部 FIXED,但开出 2 条新 Major(都只动测试/登记簿)。按预算规则(M 级 ≤2 已用满;Major 及以下由派发者筛完直接修、修完不复审、直接合)
由编排者修掉,未开第三轮。PR #719
已 squash 合并为
77b5554e。R2 开出的两条,都是"闸门测的不是我们真走的路"
MAJOR-1:canary 闸只覆盖真实 catalog 里不存在的形状。
ext-ipc.ts:797对installedDisabled提前返回、原样把 planner 结果发给 renderer;而新闸的夹具靠把
entry.source改成"alpha"让条目默认启用,只测了另一条分支。shipped catalog 的 8 条 MCP 全是
official/community,一条source:"alpha"都没有⇒ 每一次真实首装走的都是未被覆盖的那条。实证:往那条分支塞明文
secretMap,3269 条全绿。这是第零问反着用 —— 闸门覆盖的状态我们到不了,我们每次都到的状态它不覆盖。
修法:夹具加第二条条目(不改 source)做同样断言。反向验证:同一变异现在 1 pass / 1 fail。
MAJOR-2:新建的这道真闸门自己没进登记簿。
删掉整个
ext-write-channels.test.ts,登记簿完备性门与其余所有门全绿,只剩整包地板 3000 兜着 3269→3263 抓不到。且
#696行的delegates_to写-自称无委派,guarantee 却声称「全链零真值回显」—— 真判据在那个未登记的文件里,
正是登记簿自己头部写明要防的形态。修法:补登记行(精确下界 6)+
delegates_to指向它。合并后
assert-gate-files.sh报 51 个闸门文件全部在位且真的跑过。编排者一并修掉的三条 minor
"11 pass"含"1 pass"会假绿)。reload-pending映射 —— M1 新增的 timeout 路径会落该状态,此前会误报「已添加」。改为复用既有
alpha.ext.addedPendingReload。(hook 全文件零
t(),文案归extension-hub一层)。withTimeout本地化的判定(编排者自己改的那处)R2 判定与上游那份逐点等价:超时 reject 同构、
finally清 timer(探针:三档 timeout 设 60s,正常路径返回后进程 260ms 内退出 ⇒ timer 确被清)、不吞正常 rejection
(探针:超时后 40ms 才 reject,
unhandledRejection计数 0)、无 timer 泄漏。唯一差异是
label由可选改必填并删了默认文案,三个调用点都显式传 label 且末尾 catch-all不读 message ⇒ 无任何调用点行为变化。
grep确认packages/ui-mac/src现在对packages/opencode零 import。已知不修
index.ts:850的 awaitServer 超时后 fiber 仍阻塞在 Deferred 上(量级可忽略);extension-hub.tsx与use-extensions.ts里早于本 PR 的同形态硬编码中文(存量,不在射程)。