Skip to content

[REQ-128][CODE] 实现 secret prerequisite 并消除 renderer 真值回显 #696

Closed

Description

@jinjunnn

⚠️ 本期范围裁决(2026-07-30,owner)—— 优先于下方原文与基线

REQ-128 分四相,本期只做 Phase 1,只支持单组件 package(无 Bundle)。裁决见
alpha-work#49。
本票是 Phase 1 唯一的认证面(#703 MCP OAuth / #704 Alpha 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 当前会把替换后的
    liveMcp secret真值main→renderer→SDK回传;本票必须删除。
  • Catalog/Envelope只含secret id/label/required/injection target,不含值。
  • renderer值只在表单短生命周期内存;main校验declared set并写现有受限store。

Development plan

  1. strict decode Envelope auth-prerequisite profile;Desktop不decode web Declaration。
  2. 定义shared prerequisite evaluator中的secret state/reason/reference。
  3. 删除preload/result liveMcp 真值,写盘后由main/engine reload/reconnect。
  4. Manifest/receipt/log/telemetry只记录reference/status,不记录value/value digest。
  5. cancel、replacement、uninstall、missing/stale reference fail closed;完整credential
    rotation产品面不在本票。
  6. 为#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

  1. added
    type:featureNew user or system capability
    prio:P1High-value near-term delivery
    area:runtimeServices, jobs, infrastructure, or operations
    area:productUser-facing product behavior
    area:securitySecurity or access control
    area:integrationRepository, API, or external integration
    source:skill-auditOriginated from a skill or repository audit
    on Jul 30, 2026
  2. self-assigned this
    on Jul 30, 2026
  3. changed the title [-][REQ-128][CODE] 编排通用 secret、MCP OAuth 与 Alpha Connection prerequisite[/-] [+][REQ-128][CODE] 实现 secret prerequisite 并消除 renderer 真值回显[/+] on Jul 30, 2026
  4. jinjunnn commented on Jul 30, 2026

    @jinjunnn
    OwnerAuthor

    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)。

    采纳并回传实现方

    # 判定 理由
    M1 三个 await 零 timeout,且跑在 withRecoveredWrite 的串行 gated-write 链里 修 相对旧行为的退化:同样的引擎动作过去在 renderer 且有明确上界(mcp.add 15s / mcp.connect 10s / dispose 5s)。GET /mcp 在 dispose 后是冷启,每 server 受 30s 约束,8 条 MCP 合法跑到 ~30s,期间 hub 全部 gated 写排队;最坏一档 client.close() 无 timeout 且 Effect.uninterruptible,IPC 可能永不返回。修法=套仓内已有 withTimeout,超时返回已存在的 reload-pending
    M2 零回显闸门是源码文本闸门,且清单里没有 ext-ipc.ts —— 而 preload result 现在正是在那儿组装 修(最高优先级) 实测两次绕过全绿:删掉整个 mcpActivation 接线、往 IPC 结果塞 liveMcpPlaintext: intent.grants.secrets。另外 not.toContain(secretValue) 按构造恒真。这是本票存在理由的那道闸,而它是假的。 修法=在既有 ext-write-channels.test.ts 里断言返回值(canary 密钥),不动生产结构
    M3 契约文档把未接线行为写成活行为 修(纯措辞) 见下
    M4 gate-files.tsv:57 下界 11 vs 实跑 14 修 删掉 tamper/cancel/uninstall 三条仍全绿,正是该脚本自己声称要防的空闸门;同目录三条对照全是精确值
    m1 激活状态映射零测试(4 个值里 3 个从未被产生) 修 与 M1 顺路:reload-pending 正好覆盖新增的 timeout 路径

    关于 M3 的编排者裁决(重要,影响 Phase 1 的收尾判据)

    shared/package-secret-prerequisite.ts 的 5 个导出符号在模块与测试之外 0 生产调用者,
    它消费的 #694 decoder 同样还没有生产入口链 —— 因为入口链是 #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 侧引擎重载已接线 —— 这半场不降级为"声明形状"。

    已知不修 + 理由(留痕)

    finding 不修理由
    package-secret-prerequisite.ts 里 6 处无测试保护的检查(reference 键白名单、SAFE_VERSION、capability↔requiredSecrets 一致性、SAFE_SERVER、requiredSecrets 排序去重、payload/profileId 匹配) 模块尚无生产调用者;给死代码补测试不是最小动作。AC 点名的六个面(undeclared/cancel/replacement/uninstall/stale/no-value)已全部真红
    :261 的 record.status !== "ready" 不可达分支 类型收敛后的防御,删它无收益
    needs_auth / needs_client_registration 被映射为 failed 第零问拒:真 catalog 里 8 条 MCP 全是 local,经我们自己的路径到不了这两个状态
    bundle 含 MCP 子项时不触发激活 旧代码对 kind:"bundle" 同样不返回 liveMcp,存量非回归
    serverReady 是进程生命期单一 Deferred,sidecar 换端口重生会发过期 url 三个既有消费者用同一模式,非本票引入
    GATED_WRITE_CHANNELS 把 installCatalog 声明为 (intent: unknown) => Promise<unknown>,导致 mcpActivation 联合在编译期不受约束 类型层减速带;报它就是预防性抽象的要求
    同一次运行里给刚装的(默认关)MCP 开开关会落 reload-pending mcp.connect 读 boot 期配置快照,存量问题,本票既未引入也未加重
    "main 应该有 typed reload service / evaluator 折进 planner" 为让测试挂得上而重构生产结构,明确拒

    审计方的正向取证(值得记)

    真装一次带密钥的 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),不是那个行为。

  5. added a commit that references this issue on Jul 30, 2026
  6. jinjunnn commented on Jul 30, 2026

    @jinjunnn
    OwnerAuthor

    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" 会假绿)。
    • catalog MCP 分支缺 reload-pending 映射 —— M1 新增的 timeout 路径会落该状态,
      此前会误报「已添加」。改为复用既有 alpha.ext.addedPendingReload。
    • 本 PR 新引入的硬编码中文 reason 改为稳定 reason code + 新 i18n 键
      (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 的同形态硬编码中文(存量,不在射程)。

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

Metadata

Metadata

Assignees

Labels

area:integrationRepository, API, or external integrationarea:productUser-facing product behaviorarea:runtimeServices, jobs, infrastructure, or operationsarea:securitySecurity or access controlprio:P1High-value near-term deliverysource:skill-auditOriginated from a skill or repository audittype:featureNew user or system capability

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions