## 用户结果 用户在扩展中心点「安装」,能真的把一个 package 装上 —— 包括需要密钥的那种。 ## 今天卡在哪(实测,非推断) `alpha-code#713` 已交付 main 侧完整 admission:四组 digest 绑定、重取重验、 瞬时密钥采集、真实事务与写盘。端到端证据在案(密钥真落到 `alpha-mcp-secrets/<server>/v-<hex>/<VAR>`、`0600`、配置只留 `{file:}` 引用、 返回值/配置/installs.json/grants.json/journal 全不含明文)。 **但用户走不到那条链。** `extension-hub.tsx:474-477` 发出的意图是 `{catalogId, scope}` —— **没有 `attemptId`**,而 admission 要求它。 R1 审计用真注册的 IPC 喂这个真实形状,得到的是 `{ok:false, reason:"package-prerequisite-required", package:<同一张视图>}` —— **界面把同一张卡片再画一遍,没有 preview、没有密钥输入、没有安装。** ## 为什么没有票拥有它 编排者把 REQ-128 的 21 张票剪到 Phase 1 的 7 张时,验证了「用户结果 AC7 用这 7 张能不能拿到」, 但**没有逐条验证剩下这几张合起来闭合了用户可达路径**: `#695` 的范围是**呈现**,`#713` 的范围是 **main 侧** admission, renderer 的 attempt/preview/密钥采集不在任何一张的边界内。这是剪裁失误,非实现方缺陷。 ## 已勘破:不是从零做新交互 `extension-hub.tsx:2427-2450` **已有**完整的密钥采集对话框: `type="password"` 输入、`envValues()` 状态、`alpha.ext.keyPlaceholder` / `keyHint` 文案、 运行时依赖与风险提示行。legacy catalog 条目一直在用它。 **本票是把 package 的 attempt/preview 往返接进这个已有对话框**,不是新画一套。 因此**不另开设计轮**(与 `#695` 同理:已批交互上的数据源增量)。 ## Development plan 1. 详情页/卡片的 package 动作改为:先向 main 请求 **preview**(不带密钥值), 拿到 `packageAuthorization.binding` 与待确认能力集。 2. 复用现有确认对话框呈现:能力差异、组件事实、**需要哪些密钥**; 密钥输入沿用现有 `type="password"` 字段与文案。 3. 提交 confirm:只送 `attemptId` + `binding` + 瞬时密钥值,**不送 verdict/action** (`#702` 的 preflight 会拒绝 renderer 提供的这些键)。 4. 失败路径要有**行内错误面** —— `#695` R2 已记录 package 动作失败今天没有对应物 (legacy 走 `cardErr`,package 路径没有),且 `void runPackageAction(...)` 会让 reject 变成未处理拒绝。 5. 成功后刷新卡片与详情(`#695` R2 转记的「动作后新 view 回流」在这里一并闭合)。 ## Acceptance and named gates - **用户可达路径的端到端闸**:从**真实渲染的按钮点击**出发,走到 `#713` 的 admission, 断言密钥真落盘、返回值与 DOM 全程不含 canary。 ⚠️ 这条必须挂在**真实渲染路径**上(`#702`/`#695` 的 Blocker 都是「闸门在、但守的不是生产路径」)。 - 密钥值**不得**进入 renderer 全局 state、日志、DOM 属性或事件通道(断言返回值与 DOM,不断言源码文本)。 - 取消/失败后零残留:无 attempt 泄漏、无密钥落盘、行内错误可见。 - `resolve-prerequisite` 与 `install` 两种动作类各自被真正执行过 (`#695` R2 记录 `resolve-prerequisite` 从未真执行)。 ## Dependencies `alpha-code#713`(admission,本票消费它)。 ## Out of scope - 新增 UI framework / 新对话框体系 / provider 专用交互; - MCP OAuth 与 Alpha Connection 的采集面(`#703`/`#704`,Phase 2); - Bundle / Plugin。 ## Exit condition Merged PR + 上述 named gates 全绿 + 用户可达路径的端到端证据 + L2 视觉证据 (密钥采集态是**新增帧**,需补进 `req128-package-detail` 矩阵)。
Activity
[#713][CODE] package admission:digest 绑定、重取重验与端到端 secret 兑现 (#734)
jinjunnn commentedon Jul 31, 2026
R1 对抗审计记录(Opus,只读,36 组变异)+ 编排者裁决
预算:本票定级 M,审计上限 2 轮。R1 已用。修复后走 R2(只判闭合 + 修复 diff),R2 后合并。
审计判定:
RECOMMEND: BLOCK。两条 Blocker 都是闸门空缺,不是线上 bug —— 审计写探针实测生产在这两处行为是对的。
但本票对
extension-detail.tsx的全部生产改动(3 行)可整段删掉而 3314 条全绿,授权对话框里的能力差异面板也可整段删掉而全绿。
存活项扛住了全量,不是抽样误差
审计把 7 个零红存活一起打进 scratchpad 副本跑
bun test src:3127 pass / 34 fail / 19 errors,与该副本基线逐位相同。交叉验证:
grep -rln "ExtensionDetail\|ExtAuthzView"显示全仓只有ext-package-detail-wiring.cases.ts一个测试会渲染这两个组件,所以结构上不可能有别的用例捕获 B-1/B-2。
编排者裁决
B-1 详情页行内错误面零闸 → 修
删掉
extension-detail.tsx:241-243那三行 → cases 8 pass / 0 fail。那三行就是本票对该文件的 100% 改动(diff =
extension-detail.tsx | 3 ++)。更糟的是:
scripts/gate-files.tsv:57这条登记的描述明写「…新 attempt 重试、行内错误及 8 pass」。卡片那半确实被钉住(M6 → 2 fail),详情页那半一条都没有。
登记簿在替一个不存在的覆盖背书。
这是
#695R1 的形状再演一次(用例只点卡片、没点详情页)—— 只不过这次漏的是错误面而不是按钮。修法(审计已用探针跑通,约 5 行):第 7 条用例改成先进详情页再点
.alpha-ext-dsub button,断言
[data-package-detail] [role='alert']含"transport failed"。B-2 授权对话框的能力差异面板零闸,且唯一的「闸门」断言的是手写静态 HTML → 修
删掉
extension-hub.tsx:2519-2524的<ExtAuthzView … diffs={state().preview.items} />→ 8 pass / 0 fail,安装照样成功、
confirmed照样带全量 diff key(它由
preview.items构造,与渲染无关)。用户后果:在一个授权对话框里按「确认安装」,而对话框不告诉他要授予什么能力。
审计探针实测生产今天渲染的是
alpha.ext.authz-intro「Generic Remote MCP」首次安装,请求以下能力:alpha.secret-prerequisite.v1+新增chip +确认即授权上述完整能力集。删掉之后全部消失,而门不响。这是两趟 admission 流程里唯一给用户「知情」的那一面。
闸门为什么是假的:本票恰恰为这个面板给
ext-package-presentation.test.ts加了alpha.ext.authz.chipNew/alpha.ext.authz.note两个 key —— 但那条断言查的是docs/verification/.../harness/package-detail-harness.html这份手写复刻的静态 HTML里有没有这两句中文,不是生产组件渲染出没渲染。断言夹具文本而非运行结果 = 假闸门形态①。
修法:第 4 条用例里
⚠️ 别只钉一个值 ——
confirmPackageAuthorization()之前加两行 DOM 断言。package:renderer-ready的 capabilities 是空集、package:renderer-prerequisite是单能力集;在第 2 条用例里补一句「空能力集时不出现
.alpha-ext-authz-cap」,才杀得掉写死常量(形态⑨)。M-1(类)对话框渲染什么 / 何时点不动,成片无断言 → 修,走咽喉点一次收口
8 条用例断言的几乎全是
installIntents的形状。成员:packageSecretsComplete恒 trueinvalid grants,用户看到一句看不懂的失败!ok的setErrForsetAuthzBusy(true)stale or replayed),或提交中 Esc 关框而安装其实提交了按本仓「同一类第二次出现就切类模式」的规矩,走咽喉点:
在已有的
packageAuthorizationDialog()助手旁加一个expectDialogContract(dlg),一次断住 {能力行 +
新增chip + 密钥字段 + 空值时确认按钮disabled},在第 3、4 条用例各调一次;第 7 条改走详情页入口。合计约 10 行,B-1/B-2/M11/M36 一起关掉。
M19 另需约 5 行(现有
rejectPreviewOnce只造 throw,造不出{ok:false}无stage的返回)。审计给的「归一化 innerHTML 快照」加强版:不采纳。 维护噪音大,且快照不点名什么重要;
审计自己也按「修法最小」推荐前者。
M-2 密钥零回显闸维度不全 → 修,四轴全补
现有闸只扫
document.body.textContent与 IPC 返回值。M21(明文写进
<input title>)、M22(console.log明文)、M23(挂globalThis)三个变异各自全绿。审计判 console / 全局两轴超纲、只补属性轴。我不采纳这个收窄 ——
本票 AC 原文写的是「密钥值不得进入 renderer 全局 state、日志、DOM 属性或事件通道」,
四轴是我自己在 AC 里点名的。只补一轴,AC 的另外三句就是没有支撑的散文。
助手本来就要写,多两轴是几行的事。
#696R1 同形(那次零回显闸去读 7 个源文件,而真实组装点不在清单里,两个绕过全绿)。这次是维度不全而非位置不对,但被绕的方式一样。
审计实测生产是干净的(探针 P3,canary 走完整条真实链路后逐轴扫):
逐元素全部属性 +
.value=[];innerHTML/textContent= false;console.*全捕获 = 0 命中;globalThis全枚举 =[];IPC 返回值 = false;落盘全树递归扫描唯一命中
alpha-mcp-secrets/generic-remote/v-0badcafe/A_KEY本身,alpha.jsonc/ install 记录 / journal 全干净。Minor m-1 / m-2 → 不修,留痕
hub 关闭兜底 resolve、
busy/enabled守卫无闸,但生产代码是对的且必要(前者不做的话
busy永久卡住、该包按钮整个 session 点不动);属「主动加的防御分支无测试」,不拦合并。Minor m-3 / m-4 → 归 L2,主 session 采集
extension-hub.css:1020的overflow-wrap: anywhere只在静态 harness 可见;matrix P6「长 prerequisite label」截图待采。
「留空」文案 → 不在本票改,另开 P2
审计跑了真实渲染真实按钮:空 / 纯空格 → 按钮
disabled = true;非空 →false。同屏文案是
需要密钥(留空则装好后再配)+ placeholder粘贴密钥…(可留空)。按判据:按钮禁用 ⇒ 文案与行为不一致(困惑,但点不坏),不是 Major。
真实代价是用户看到「可留空」、按钮却是灰的、且没有任何一句话解释为什么灰 —— 死胡同,不是数据损坏。
不在本票改的理由已核:
alpha.ext.confirmEnv/alpha.ext.keyPlaceholder与 legacy 条目安装对话框共用,那条路径上留空确实合法。要修得新开 key 或改 legacy 语义,两者都超出「补 renderer 那半」的边界。
审计核过、确认没问题的(射程记录)
#695那个形态在接线层已经堵住,只有错误面那层漏了(= B-1)。install走详情页(用例 2)+ 卡片(用例 7);resolve-prerequisite走卡片(用例 3/4/5/6),真写出 0600 密钥文件、真翻前置条件状态。不是 mock —— 真
createPackageAdmissionCoordinator、真runCatalogInstallWithPackagePreflight、真注册的 IPC handler、真事务、真 secret store;
installLegacy直接throw,漏到 legacy planner 会立刻红。preauthorized场景)、B(M30 首趟塞 secrets → 5 fail,
Object.keys().sort()全等断言)、C(M2 写死 attemptId → 2 fail,用例 5/6 断言取消与失败后都换新 id)、
D(M5/M16/M31 全红)。
D 的
required:false子情形审计查了package-installability.ts:150——if (!component.required)直接判 blocked,Phase 1 结构上不存在可安装的required:false前置条件。第零问:到不了,不开。binding原样带回:M17 篡改capabilityDigest→ 4 fail。alpha-mcp-secrets不存在、alpha.jsonc不存在、canary 不在 DOM;加 P3 全树落盘扫描。泄漏的 attempt 只留在 main 的
attemptsMap,MAX_ATTEMPTS=64有界。delegates_to指向的package-admission.test.ts也在册。preload/零改动(那条「三个 import specifier +exposeInMainWorld恰好 1 次」的闸没被碰);package.json/bun.lock零改动;diff 里ipcRenderer/ipcMain/GATED_WRITE_CHANNELS新增行为 0;extension-detail.tsx只加 3 行、没有为挂测试重构分支结构;无as any/@ts-/TODO;未触UPSTREAM_PATHS。open={!!confirming() || !!packageAuthz()}改写里 legacy body 分支加了
!packageAuthz() &&前缀,packageAuthz()为 null 时行为逐字不变。这一轮最值得记的一条:审计自己撞上第零问并主动撤回
审计本来准备把 Esc 键守卫开成 Blocker ——
extension-hub.tsx:632的if (confirming() || authz() || confirmBusy() || authzBusy()) return白纸黑字写着「授权框在场…hub 层绝不消费 Esc —— 否则外层先关掉整个 hub」,
而本票新增的模态状态没有登记进去。教科书级的「枚举漏,最先漏掉的是我们自己新产生的东西」。
跑了一遍才发现到不了:
dialog-core.ts:171用addEventListener(…, true)挂在捕获相并
stopImmediatePropagation(),而 hub 的onKey是冒泡相、焦点又被 Dialog 圈在面板内⇒ hub 的监听器根本不会执行。探针实测:卡片入口 Esc → 对话框关、
onClose0 次;详情页入口 Esc → 对话框关、详情页仍在、
onClose0 次;对照组(无对话框)Esc →
onClose1 次(证明探针接线有效)。审计原话:「论证越充分越容易跳过第零问 —— 这条就是。」
审计已看过、主动丢掉的
label去重(零红存活):要触发需同包声明两个label相同而prerequisiteId不同的密钥;label就是变量名,在签名 payload 里唯一且已排序去重。到不了,且即便到得了 main 会secret-undeclaredfail-closed。stage === "authorize"判别(零红存活):与后面的"packageAuthorization" in result &&在联合类型上等价,纯冗余收窄。setEnvValues({}):两处互为兜底,同时删就红。冗余防御。authz({host:"confirm"})与packageAuthz()同时非空会渲染两个ExtAuthzView:要到达需两个模态流程同时在场,而两者共用同一个模态 Dialog、各自关闭时清各自 state,底层被
inert屏蔽。到不了。packageAuthz抽成独立组件/状态机(#695已拒过一次的形状);②给
ExtAuthzView建通用契约测试框架;③为 Phase 2/3 预留 multi-component preview 渲染路径;④纯风格。审计自己跑的门(真实输出)
审计结束时
git status --porcelain与开工时逐字相同,全部变异只在 scratchpad 副本上做。待办(修复清单)
[role='alert']断言(~5 行).alpha-ext-authz-cap」(~3 行)expectDialogContract(dlg),第 3/4 条各调一次(~10 行,连带关掉 M11/M36){ok:false}无stage场景(~5 行)expectNoCanaryAnywhere()助手,四轴:元素全部属性 +.value、innerHTML、console.*捕获、globalThis枚举(~10 行)[#732][DOC] 采集 P6 密钥采集帧,并如实记录两处需 owner 判断的观察
[#732][CODE] renderer 侧 package 安装:attempt/preview 往返与密钥采集接线 (#742)
jinjunnn commentedon Jul 31, 2026
R2 审计记录(Opus,只读)+ 编排者裁决 + 已合并
预算:M 级 ≤2 轮,两轮用满。R2 判
RECOMMEND: MERGE。R1 八条逐条闭合(审计独立重做,与自报逐位吻合,无一虚报)
ExtAuthzViewpackageSecretsComplete恒真setAuthzBusy(true)!ok的setErrForconsole.logglobalThis审计另补做四个不在 R1 表内的:明文进渲染 HTML(7/1)、删卡片行内错误面(6/2)、
删
runPackageAction的 catch(7/1)、用例数 8→7 时 wrapper 立刻红。新闸门自身的真伪(R2 的核心价值)
expectDialogContract两种能力集都挂了,写死杀得掉:审计真去把diffs写死成单条常量 → 7/1 红在用例 2;写死成
[]→ 7/1 红在用例 3;把packagePrerequisites()写死 → 4 pass / 4 fail。两个方向都红 ⇒ 没有任何单一常量能同时满足两处期望。
console劫持在finally恢复。try块,新旧断言集合逐条对齐数过「一条不少」;两处
body.textContent换成expectNoCanaryAnywhere(含
innerHTML,是textContent的超集)。粒度没被吞并(助手内部仍是 6 个独立expect)。编排者裁决:R2 的两条 minor 修了
预算已用尽,按治理默认:minor 筛完直接修、修完不复审直接合。由编排者亲自修 + 变异自检。
minor-1「破坏它 = 破坏观测它」
canary 扫描为避开用户刚输入的密钥,按选择器把所有
.alpha-ext-key-input[type=password]的 value 清空再扫 —— 而那恰好是唯一合法装密钥的 class。审计实测:生产多渲染一个同 class 的 password 框装明文 → 8 pass / 0 fail 完全隐形;
对照组换成
class="leak-other"→ 7/1 抓得到。改成按身份遮蔽:只遮蔽
fillPackageSecret真正写入的那一个元素,其余一律照扫。minor-2 日志/全局轴只下潜一层
审计实测存活的形状:
console.debug("intent",{catalogId,grants:{secrets}})、console.error(new Error(secret))、globalThis.__leak={a:{b:secret}}、console.warn(new Map(...))。而
gate-files.tsv:57写的是「日志/全局 state 零回显」——正是本轮 B-1 被判 Blocker 的同一个毛病:登记簿替不存在的覆盖背书。 不修就是重犯。
ownScalarValues带深度预算(4)继续下潜,Error取 message、Map/Set展开条目。minor-3(登记描述)随之成立,无需另改。
第零问记录:今天
renderer/extensions/下 grep 不到任何console.*调用,所以这是防未来回归的棘轮,不是现行缺陷 —— 但棘轮的价值正在于此。
编排者的变异自检(五条,每条先确认落地再看结果)
console深度 2{grants:{secrets}}console.error(new Error(secret))globalThis深度 2{a:{b:secret}}input title(R1 原变异)审计主动丢掉的(编排者复核后认同)
归一化 HTML 快照加强版、m-1/m-2 防御分支无测试、把
packageAuthz抽成组件/状态机(
#695已拒过的形状)、「留空」文案(已另开 P2)、三处构造上不可达的零红存活。out-of-round(不参与合并判定,记录备查)
packageSecretsComplete用.trim().length > 0判完整,但提交的是未 trim 的原值 ——纯空白被拦、首尾空格原样写盘。审计认为原样保留用户输入是对的,只是判据与提交值不同源。
[ext-transaction] tx-committed日志,失败时诊断少一截。expectNoCanaryAnywhere走querySelectorAll("*"),扫不到游离节点与 Solid signal 闭包 ——canary 扫描的固有边界,非本次引入。
合并
PR #742,squash 为
bbff1ea8。rebase 到origin/alpha(含#729)后全量复验:bun test src3318 pass / 0 fail、cases 8 pass、assert-gate-files60、tsgo202 = base、文档链接 ✓、diff --check干净、与UPSTREAM_PATHS零交集。rebase 时
matrix.md有实质冲突:#732那版写的「update-required 不带 presentation、名称回退为
package:next-profile-agent」是#729合并之前的事实,现已为假。已合并两者并注明旧行为的时间范围 —— 冲突解决不是选一边,是让文本重新变真。
jinjunnn commentedon Jul 31, 2026
订正:上面 R1/R2 记录里写的「留空文案已另开 P2 票」当时并未真的开票 —— 我把打算做的事写成了已做的事。现已补开
#743(P2)。裁决本身不变(不在
#732内改,因为alpha.ext.confirmEnv/alpha.ext.keyPlaceholder与 legacy 安装对话框共用 key,那条路径上留空确实合法),变的只是它现在真的有票拥有它了。[#737][CODE] 消除宿主为契约文法写的替身:枚举整类,收口到共享 predicate (#747)