fix: the setup card must say what setup resets

Restarting dsh with the 2.0.12 copy still in the profile re-provisioned
the bundle to 2.0.12's pin: REA went back to 6.7.0, the Ghidra gate
override went with it, and the MCP row failed with "REA could not start".
That is the card doing its job on a machine whose plugin copy was stale —
but the card never said the aligned run also wipes the override.

- client.js: third note in both languages. It names the override case and
  the command that restores it, so the card is not advice that silently
  undoes a deliberate choice.
- README (both): the "you must run setup" block says the same thing.
- ghidra-gate-override.mjs: judge a recorded override by the files on disk.
  Re-provisioning replaces rea-agents while leaving the record behind, so
  --enable now replaces a stale record instead of refusing, and --disable
  counts already-lost files instead of throwing.
- CHANGELOG: 2.0.13 records both, plus the failure that prompted them.

Verified after re-aligning this machine: setup --reuse reached
"setup complete" with its Ghidra smoke importing java.exe in 28 s on 12.2,
the shim resolves REA 6.8.0, and the MCP path through the shim returns the
same segments / procedure inventory / entry pseudocode as the 12.1.4
baseline with provider.version = 12.2.

Gates: audit 24/24 hash-pinned clean, 88 tests (85 pass / 3 skipped),
verify-branch ALL CHECKS PASSED, host-compat 10 covered / 5 outside.
This commit is contained in:
czcdr committed 2026-10-26 21:16:33 +08:00
1 parent 10572a7958
commit c55768233c
5 files changed
+53 -4

No files matched your search

+9
View File
@@ -38,6 +38,15 @@
- **`check-updates.ps1` 认得这份记录**:盘上跑 12.2 时打印「本机覆盖 Ghidra 闸门」,而不是劝用户「重跑 setup 对齐」
—— 后者正好会把覆盖悄悄抹掉。
### 修复
- **「还要跑一次 setup」卡片的说明补上覆盖这一条**:这张卡片出现在每次升级之后,而它就是叫你去跑 setup 的。
setup 会重新供给内置 REA,**Ghidra 闸门覆盖随之消失** —— 卡片现在明写这一点并给出恢复命令。踩到的实例:
本机重启后,profile 里还是 2.0.12 的旧副本,它的 shim 按自己的 pin 把 bundle 重新供给回 6.7.0,
覆盖与 6.8.0 一起被抹掉,MCP 行随之 `REA could not start`。
- **`ghidra-gate-override.mjs` 处理「记录还在、补丁已被 re-provision 抹掉」的半状态**:`--enable` 现在按盘上
实际内容判断记录是否仍成立,陈旧记录直接替换而不是报错;`--disable` 对已失去补丁的文件只计数不抛错。
### 验证
- 单测 **88 条**(85 过 / 3 平台跳过 / 0 失败);`audit-manifest` **24/24 hash-pinned clean**;
+2
View File
@@ -32,6 +32,8 @@
> does **not** move the bundle to the new pins — REA and the engines stay at the versions recorded by the last
> setup run. Skip it on a first install and the row exits `MODULE_NOT_FOUND` with no tools and no message;
> skip it after an upgrade and you keep running the old REA silently (`check-updates.ps1` reports the drift).
> If you had enabled the Ghidra gate override (see §9 of the 2026-10-26 review), that setup run re-provisions the
> bundled REA and the patch goes with it — re-enable it afterwards, or accept the pinned Ghidra again.
> The full rule set is under [Updating](#updating--three-independent-things).
> **A first install and every plugin upgrade also show a one-time guide in the Web GUI.** It says whether the
+2 -1
View File
@@ -29,7 +29,8 @@
> 市场里的安装/更新只替换包,然后到此为止。从 **2.0.8** 起,市场升级能保证**那一行**继续可用(镜像启动器会
> 转发执行新的那份),但它**不会**把 bundle 换到新的钉子 —— REA 与引擎仍是上一次 setup 时记下的版本。
> 首次安装不跑:行以 `MODULE_NOT_FOUND` 退出,工具消失且没有任何提示;升级后不跑:仍旧跑旧 REA 且没有症状
>(`check-updates.ps1` 会报出这条漂移)。完整规则见[更新](#更新--三件互相独立的事)。
>(`check-updates.ps1` 会报出这条漂移)。若你开过 Ghidra 闸门覆盖,这一次 setup 会重新供给内置 REA、覆盖随之
> 消失 —— 跑完再 `--enable` 恢复,或者接受回到钉住的那一版。完整规则见[更新](#更新--三件互相独立的事)。
> **首次安装与每次升级插件后,Web GUI 会显示一次操作指南。** 它会说明运行时是缺失、还是落后于本插件钉的版本,
> 给出这次安装对应的完整 `node "<安装路径>/scripts/setup.mjs"` 命令,并按插件版本记住“已看过”。
+3
View File
@@ -56,6 +56,7 @@ window.__ModuleLoader__.load({
hint: "跑完重启 dsh。这一步只对齐运行时,不会更新插件包本身;升级后同样再跑一次。",
noteSmoke: "setup 结束时会用 JDK 自带的 java 跑一次真实分析来验证引擎 —— 那里 30–60 秒没有任何输出是正常的。",
noteLock: "若它报 “another provisioning run holds …”,说明后台已有一个 setup 在跑,等它结束再试即可。",
noteOverride: "若你开过 Ghidra 闸门覆盖(让本机跑 REA 未验证的 Ghidra),setup 会重新供给内置 REA,那层覆盖随之消失 —— 跑完执行 node scripts/ghidra-gate-override.mjs --enable --version 12.2 恢复。",
},
en: {
badge: "ONE-TIME SETUP",
@@ -77,6 +78,7 @@ window.__ModuleLoader__.load({
hint: "Restart dsh afterwards. This only aligns the runtime; it does not update the plugin package — run it again after the next upgrade.",
noteSmoke: "It finishes by importing a small file to prove the analysis engine works — 30–60 s with no output there is normal.",
noteLock: "If it reports “another provisioning run holds …”, a setup is already running in the background; wait for it.",
noteOverride: "If you had enabled the Ghidra gate override (running a Ghidra REA has not verified), setup re-provisions the bundled REA and the patch goes with it — run node scripts/ghidra-gate-override.mjs --enable --version 12.2 afterwards to restore it.",
},
};
@@ -354,6 +356,7 @@ window.__ModuleLoader__.load({
},
h("li", null, t.noteSmoke),
h("li", null, t.noteLock),
h("li", null, t.noteOverride),
),
h(
+37 -3
View File
@@ -138,11 +138,30 @@ function main() {
console.log("no override is recorded; nothing to restore");
return;
}
let restored = 0;
let stale = 0;
for (const entry of active.files) {
replaceValue(path.join(vendor, entry.file), entry.patched, entry.original);
const file = path.join(vendor, entry.file);
if (!existsSync(file)) {
stale += 1;
continue;
}
const text = readFileSync(file, "utf8");
if (new RegExp(`${CONSTANT}\\s*=\\s*"${entry.patched.replace(/\./g, "\\.")}"`).test(text)) {
replaceValue(file, entry.patched, entry.original);
restored += 1;
} else if (new RegExp(`${CONSTANT}\\s*=\\s*"${entry.original.replace(/\./g, "\\.")}"`).test(text)) {
stale += 1;
} else {
throw new Error(`${entry.file} declares neither ${entry.patched} nor ${entry.original}; inspect it before restoring`);
}
}
unlinkSync(record);
console.log(`restored ${CONSTANT} = "${active.originalVersion}" in ${active.files.length} file(s); override record removed`);
console.log(
`restored ${CONSTANT} = "${active.originalVersion}" in ${restored} file(s)` +
(stale > 0 ? `; ${stale} file(s) had already been replaced by a re-provision` : "") +
"; override record removed",
);
return;
}
@@ -155,7 +174,22 @@ function main() {
console.log(`already ${version} — nothing to do`);
return;
}
if (active) throw new Error(`an override is already recorded (→ ${active.requestedVersion}); run --disable first`);
if (active) {
// Re-provisioning rea-agents replaces the tree, so a record can outlive the patch
// it describes. That stale record must not block the next --enable, and it must
// not be trusted by --disable either: judge it by the files on disk.
const stillApplied = active.files.every((entry) => {
const file = path.join(vendor, entry.file);
if (!existsSync(file)) return false;
const text = readFileSync(file, "utf8");
return new RegExp(`${CONSTANT}\\s*=\\s*"${entry.patched.replace(/\./g, "\\.")}"`).test(text);
});
if (stillApplied) {
throw new Error(`an override is already recorded (→ ${active.requestedVersion}); run --disable first`);
}
console.log(`previous record is stale (rea-agents was re-provisioned); replacing it`);
unlinkSync(record);
}
const files = [];
for (const entry of declarations) {