@hanamesh/dsh-app-host 0.6.21 → 0.6.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +19 -15
  2. package/dist/client-ui.js +8 -16
  3. package/dist/dsh-compat.js +1 -1
  4. package/dist/dsh.d.ts +1 -2
  5. package/dist/dsh.js +5 -9
  6. package/dist/git-command-worker.js +12 -0
  7. package/dist/git-process.js +8 -3
  8. package/dist/git-worker.js +37 -7
  9. package/dist/install-target/suite.js +3 -3
  10. package/dist/installation-status.d.ts +0 -8
  11. package/dist/library/diagnostics.js +0 -1
  12. package/dist/library/github-target.js +1 -1
  13. package/dist/library/handoff.js +3 -11
  14. package/dist/library/install-target-http.js +39 -0
  15. package/dist/library/install.js +13 -60
  16. package/dist/library/installation-cli.js +41 -16
  17. package/dist/library/installation-command-worker.js +3 -2
  18. package/dist/library/installation-discovery.js +95 -0
  19. package/dist/library/installation-status.js +14 -13
  20. package/dist/library/installation-worker.js +72 -14
  21. package/dist/library/locate.js +4 -13
  22. package/dist/library/routes.js +2 -10
  23. package/dist/library/service.js +5 -5
  24. package/dist/library/suite.js +9 -10
  25. package/dist/node-runtime/service.js +4 -4
  26. package/dist/router/sources.js +5 -2
  27. package/dist/runtime.js +15 -1
  28. package/dist/store.js +12 -5
  29. package/dist/windows-job.js +30 -0
  30. package/dist/windows-job.ps1 +75 -0
  31. package/docs/APP_PACKAGE.md +1 -1
  32. package/docs/CONTRACT.md +7 -7
  33. package/docs/DSH_COMPATIBILITY.md +1 -1
  34. package/docs/INSTALLATION_STATUS.md +29 -18
  35. package/docs/INSTALL_TARGET.md +9 -5
  36. package/docs/LIBRARY.md +3 -14
  37. package/docs/LICENSES.json +207 -69
  38. package/docs/SECURITY.md +16 -1
  39. package/package.json +41 -27
package/README.md CHANGED
@@ -1,6 +1,16 @@
1
+ PX3 跨平台候选:Windows 可以运行插件/公开服务、attach 应用、受控只读 CLI 与普通包安装;`owned` 应用仍在创建进程、数据目录及请求凭据之前明确拒绝 `PLATFORM_UNSUPPORTED`。Windows 单测保留真实 attach 的 HTTP/租约/网关断言,并验证 owned 的持久失败、无进程/文件写入及不消费凭据;这些断言不构成 Windows owned 运行支持。独立 Node 可用不扩大 owned 支持;受管 Node 准备仍遵守原平台限制。Windows 本地快照采用文件 flush 加同目录 rename,不承诺断电后目录项持久;Coding OAuth Gateway 文件源因缺私有 ACL 证明继续拒绝,HTTP 提供方不变。各平台实际结果以本分支 GitHub Actions 矩阵收据为准。
2
+
3
+ R115返修(同0.6.23候选):登录shell按退出结果和封装JSON解析成功,忽略正常stderr;所选CLI保留管理器PATH与XDG目录/缺省,不再用空临时XDG隐藏全局版本。真实Bash/asdf/mise启动链回归与工具来源见 [安装状态](docs/INSTALLATION_STATUS.md)。
4
+
5
+ 0.6.23 候选:install-target 对名单外有效网站提供精确CORS的固定403来源拒绝,GET预检以204和按需PNA头支持浏览器读回;不开放能力/清单/安装权限。同意检测后自动发现当前用户的 Claude Code / Codex 官方 CLI,无手动读取器入口;接受不低于 2.1.301 / 0.162.1 且版本格式与清单 JSON 校验通过的版本。安装布局、优先级、只读边界及具名预算见 [安装状态](docs/INSTALLATION_STATUS.md)。
6
+
7
+ PK3 开发供给:`vendor/hanamesh-core-0.3.33.tgz` 是 Core 分支提交 `969a89a1f7e383fca7d6aefa7f27507d3a1605d5` 的正常归档,仅供开发与公开合约测试;SHA256 和来源见 `vendor/core-fixture.json`,安装仍严格检查真实 peers。这不是已发布的 npm Core,也不进入运行时依赖。
8
+
9
+ PK1 兼容候选:开发输入更新为 DSH 0.2.2-rc.1 / Cordis 4.1.0 / Schemastery 3.19.0(适用者)。公开 peer 保留兼容范围,并显式包含 0.2.2 预发布;不锁定用户宿主版本。本分支尚未发布,安装和原生加载结果以 PK1 交件为准。
10
+
1
11
  0.6.21 候选:普通市场和网站交接消费 Indexer 的固定 GitHub SHA/子目录/包名/构建名单。客户端沿原确认页展示来源、完整 commit 与本机构建提示;确认回传同一目标,目录更新不得换 SHA。执行仍走受控官方 DSH CLI / pnpm,读回精确来源与启用项后报告需完全退出重开;没有改上游或另写安装器。详见 [安装目标](docs/INSTALL_TARGET.md)。初轮已完成隔离 DSH、网站及真实 DTO 验收;本轮 R25 另修未匹配固定目标时的拒绝与退休目录版本字段,仍待复审,不宣称已发布。
2
12
 
3
- 0.6.16 候选:普通市场通过注入的宿主 pluginManager.registries() 读取源事实,并按宿主锚点/版本守卫加载公开 ./registry 规则模块;AppHost 不声明、安装或捆绑 manager。发布依赖/peer闭包与0.6.15一致。pnpm自有源(null)不显式传--registry,配置镜像则传同一源;查询30秒具名超时,脱敏原诊断同进UI和日志。
13
+ 0.6.22 候选:普通插件只执行目录固定目标;缺 installMethods 即拒绝。套件安装命令只消费 Core,删除普通包名的元数据查询和规则加载。
4
14
 
5
15
  0.6.15 候选:市场保留官方 CLI 的 stdout/stderr 真实失败原因并脱敏,在卡片与安装确认的技术详情中显示。正文显示 pnpm 真实错误类别;宿主发布时间检查失败时列出受影响包与实际 cutoff;HanaMesh 总包安装/升级使用精确发布版本,发布后即可验收;不修改 profile、pnpm 供应链策略或安装执行路径。
6
16
 
@@ -82,7 +92,7 @@ CLI-8:未配置来源的新用户默认请求 HanaMesh 视图(`https://marke
82
92
 
83
93
  > 分支 `t6-usage-receipts`(2026-09-22,设计定案 §3「协议层」,T6,**未发版**,由派发 session 合入下一 rc):**Router 使用回执。** app-host 自己的 storage domain `hanamesh_router_receipts` 按小时记 `{appId, providerId, model, hour, count}`(Router 每次注入记路由与 `injections`,网关每次转发 `count+1`;不记内容、不记值),`GET /hanamesh/router/receipts[?appId=]` 可查本地账。usage 席位上报的 `use` 事件改为在 **UTC 小时结束或应用最后一个活实例停止时**发出(`occurredAt` 仍是该小时首个转发请求;同 key 一小时一条不变),并附 `targetRef=appId` 与该小时的 `receipt {providerId, model, count}`;无席位只记本地账不报错;宿主死机前未报的小时在下次启动由账本补报(席位按 key 去重)。需要 usage `0.2.0-rc.7`(席位接受三个可选键;更早版本会 `rejected INVALID_RECORD_INPUT`,只 debug 日志)。
84
94
 
85
- > rc.28(2026-09-22,设计定案 §4「HanaMesh 市场」,T4):**「应用库」升级为 HanaMesh 市场——插件与应用同一入口、同一安装路径。** 侧栏入口与页面标题改为「市场」/「HanaMesh 市场」,设置段「应用库来源」改名「市场目录源」。目录默认列出**全部**类别(客户端显式 `category=`),筛选 = 全部 / 应用(`hanamesh-app`)/ 插件(客户端视图:非应用的 npm 条目)/ 目录页里出现过的每个类别;搜索、「更多」保留。每条目带 `kind`(`application` / `plugin` / `listing`)、`installed`(已装行)与 `upgradeAvailable`(目录 `latestVersion` 严格新于已装版本,内建 semver 比较,无依赖)。卡片状态:未装「安装」/ 已装(应用「打开」「卸载」,插件「卸载」)/ 可升级「升级到 x.y.z」(同一 `dsh plugin add --save-exact` 路径)/ 需重启。**已装列表含插件**:扫 profile `package.json` 的 `dependencies`(减去应用包),`bundle`(声明 `dsh.bundle`)与 `active`(在 `dsh.profile.bundles`);「需重启」由本插件启动时的依赖快照推出:启动后新增 → `installed-not-loaded`,启动后移除 → `uninstalled-not-unloaded`。任一操作完成或列表含上述状态时页面顶部出现「需重启 DSH」:在桌面壳内(DSH 页在壳 iframe 里)给出 `hanamesh://restart` 深链按钮,官方 DSH 直开浏览器时只提示「请手动重启 DSH」。新路由:`GET /hanamesh/library/installedPlugins` → `{plugins,apps,restartRequired}`;`POST /hanamesh/library/plugins/install {packageName}`(须在启用目录里能搜到该包;应用包走应用路径)与 `POST /hanamesh/library/plugins/uninstall {packageName}`(应用包转应用卸载:runtime + 运行中检查)→ 202 + 事件流;`POST /hanamesh/library/install` 现接受 `{itemId, packageName?}`(`packageName` 用于目录精确查找,插件条目也可装)。旧路由全部保留。**修 `LIB-PROVISION-INPUT`**:`provision` / `uninstall` / `plugins/*` 缺字段或字段非法 → 400 `INVALID_INPUT`,操作不启动(此前 TypeError → 事件 `LIBRARY_OPERATION_FAILED`)。受保护包 `hanamesh-core` / `hanamesh-usage` / `@hanamesh/dsh-app-host`(含旧写法)永不经市场装卸(`PACKAGE_DENIED`;rc.27 前阻止名单写的是从未存在过的 `@hanamesh/dsh-core`)。不做 T5(`provide('market')`、dshmarket 互斥、recommender 接线)。AH-M01–M08 测试,M13/M14 变异;真实门见 `docs/acceptance/t4-market-20260922/`。
95
+ > rc.28(2026-09-22,设计定案 §4「HanaMesh 市场」,T4):**「应用库」升级为 HanaMesh 市场——插件与应用同一入口、同一安装路径。** 侧栏入口与页面标题改为「市场」/「HanaMesh 市场」,设置段「应用库来源」改名「市场目录源」。目录默认列出**全部**类别(客户端显式 `category=`),筛选 = 全部 / 应用(`hanamesh-app`)/ 插件(客户端视图:非应用的 npm 条目)/ 目录页里出现过的每个类别;搜索、「更多」保留。每条目带 `kind`(`application` / `plugin` / `listing`)、`installed`(已装行)与 `upgradeAvailable`(审核 npm `installTarget.version` 严格新于已装版本)。卡片状态:未装「安装」/ 已装(应用「打开」「卸载」,插件「卸载」)/ 可升级「升级」(同一 `dsh plugin add --save-exact` 路径)/ 需重启。**已装列表含插件**:扫 profile `package.json` 的 `dependencies`(减去应用包),`bundle`(声明 `dsh.bundle`)与 `active`(在 `dsh.profile.bundles`);「需重启」由本插件启动时的依赖快照推出:启动后新增 → `installed-not-loaded`,启动后移除 → `uninstalled-not-unloaded`。任一操作完成或列表含上述状态时页面顶部出现「需重启 DSH」:在桌面壳内(DSH 页在壳 iframe 里)给出 `hanamesh://restart` 深链按钮,官方 DSH 直开浏览器时只提示「请手动重启 DSH」。新路由:`GET /hanamesh/library/installedPlugins` → `{plugins,apps,restartRequired}`;`POST /hanamesh/library/plugins/install {packageName}`(须在启用目录里能搜到该包;应用包走应用路径)与 `POST /hanamesh/library/plugins/uninstall {packageName}`(应用包转应用卸载:runtime + 运行中检查)→ 202 + 事件流;`POST /hanamesh/library/install` 现接受 `{itemId, packageName?}`(`packageName` 用于目录精确查找,插件条目也可装)。旧路由全部保留。**修 `LIB-PROVISION-INPUT`**:`provision` / `uninstall` / `plugins/*` 缺字段或字段非法 → 400 `INVALID_INPUT`,操作不启动(此前 TypeError → 事件 `LIBRARY_OPERATION_FAILED`)。受保护包 `hanamesh-core` / `hanamesh-usage` / `@hanamesh/dsh-app-host`(含旧写法)永不经市场装卸(`PACKAGE_DENIED`;rc.27 前阻止名单写的是从未存在过的 `@hanamesh/dsh-core`)。不做 T5(`provide('market')`、dshmarket 互斥、recommender 接线)。AH-M01–M08 测试,M13/M14 变异;真实门见 `docs/acceptance/t4-market-20260922/`。
86
96
 
87
97
  > rc.27(2026-09-21,用户定,STATUS `P2-USE-EVENTS`):**应用的使用证据由 app-host 上报。** 此前 app-host 从不调用 usage 插件的 record 座位,应用既没有 `open` 也没有 `use` 事件。现在:`ctx.inject(['hanameshUsage'])` 可选取得座位(鸭子类型 `record()`,三插件仍互不 import;缺席/不兼容/抛错/`rejected` 只 debug 日志,永不阻止启动或转发);`open` = 实例到达 `ready` 一次(`open:<appId>:<instanceId>`);`use` = 该实例网关真正转发了已授权请求且应用回应非 401/403,每应用每 UTC 小时一次(`use:<appId>:<YYYYMMDDHH>`,时间取桶内第一个请求);引导票据、网关拒绝、宿主就绪探针不计。`hanaRef` = 应用 npm 包名:描述符新增可选 `packageName`(向后兼容),缺省由启动时的已安装扫描 `host.bindPackageName()` 绑定,两者都没有则该应用不产生证据。`sourcePlugin = '@hanamesh/dsh-app-host'`。新面:`host.packageName/bindPackageName/onActivity`、`FixedGateway({onForward})`、`createUsageEvidence()`、`list().apps[].packageName`。契约见 `docs/CONTRACT.md`「应用使用证据」;AH-U01–U07 测试(U07 为真实 Cordis 根,座位先于/后于本插件提供两种顺序),M11/M12 变异。
88
98
 
@@ -98,7 +108,7 @@ CLI-8:未配置来源的新用户默认请求 HanaMesh 视图(`https://marke
98
108
  >
99
109
  > rc.19(2026-09-21):网关不再只认 cookie——嵌入式 webview(HanaMesh 桌面 Tauri/WKWebView、开了跟踪防护的 WebView2)里应用 iframe 相对壳的顶层 origin 是第三方,`SameSite=Strict` 的引导 cookie 被丢弃,303 之后每个请求都 403(响应带 `frame-ancestors 'none'`)→ 用户看到空白 iframe(2026-09-21 桌面 rc.4 + Vibe 实测)。rc.19 在 cookie 缺席时改用 Fetch Metadata:`sec-fetch-site: same-origin`(应用自身子资源/XHR)、或 `same-site` + `iframe` + 父 origin referer(引导后的框架导航)、或应用 origin 的 WebSocket 握手,且必须已有一张为仍在有效期 lease 消费过的引导票;cross-site / `none`(地址栏)一律拒绝。H15 三条测试;浏览器直开仍走 cookie。
100
110
  >
101
- > rc.18(2026-09-20):应用库卡片跟踪安装/补齐运行时/卸载操作——按钮进入「…中…」,订阅 `/hanamesh/library/events` 直到 `-done`/`-failed`,失败时在卡片上显示错误码与可读原因(如 `REGISTRY_LOOKUP_FAILED`:应用包不在当前 registry),并提供「重试安装」。用户 2026-09-20 实测:点安装无任何反应。宿主逻辑不变。
111
+ > rc.18(2026-09-20):应用库卡片跟踪安装/补齐运行时/卸载操作——按钮进入「…中…」,订阅 `/hanamesh/library/events` 直到 `-done`/`-failed`,失败时在卡片上显示错误码与可读原因(如 `NOT_INSTALLABLE`:目录没有审核过的固定目标),并提供「重试安装」。用户 2026-09-20 实测:点安装无任何反应。宿主逻辑不变。
102
112
 
103
113
  **交付状态:rc.3 用户 ACCEPTED 2026-09-13;rc.4–rc.12 为增量 🧪;rc.13 只补上架前提(MIT 许可证、`repository` 字段、DSH peer 精确钉 `0.1.5-alpha.1`);rc.14 删除客户端里对 `hanameshCore` 的死读取(浏览器侧 cordis 上下文不含宿主服务,该读取恒为 null,且把「未安装 HanaMesh Core」误显给已装用户)——本包对 Core 现在零引用,Core 状态只在 Core 自己的设置段;仍待用户验收。**
104
114
 
@@ -224,20 +234,14 @@ Actions runs on the standard public/free `macos-15` arm64 runner because the exi
224
234
 
225
235
  ## 发布检查:平台依赖闭包(不可关闭)
226
236
 
227
- `DSH_PNPM_PLATFORM_CLOSURE` 是正确性不变量,Mac/Windows/Linux 同样适用:DSH 自带 pnpm 11.7.0 处理不适用平台 optional 子包时可能安装完不退出,导致 DSH 持续等待。正常 tarball、bundled 包及完整锁定依赖闭包发现 os/cpu/libc 字段或平台子包 optionalDependencies 即拒绝,报具体包名/路径;缺闭包证据也拒绝。不能以开关、超时、重试、omit-optional 或静默兜底绕过;应改用单包内含多平台文件的中立依赖。
228
-
229
- 运行 `npm run release:check` 正常打包并检查;发布仅用 `npm run release:publish`,发布已检查的同一 tarball。直接目录 npm publish 会被 prepublishOnly 明确拒绝。开发工具 `@hanamesh/release-platform-gate` 仅 devDependency,先按接线请求复制经 SHA 固定的正常工具包并更新冻结锁;CI 必须接入这个发布门。详细输出包含不可关闭的原因、正常包/锁 SHA 和扫描路径;absent optional peer 按 npm 定义列明,不等同 optionalDependencies。
230
-
231
- 共享门已升级到 R2 PASS 的 0.1.10,原正常工具 tar SHA256 为 `c72010c96a5f22e36d65f6c1fd850e583dead7e843644eba1657b4416d9cbd0d`;最终 main 来源见 `vendor/release-platform-gate.json`。本仓接线仍须 R1 独审,不等于 npm 发布放行,本次不发布。
232
-
233
- `verify:pack` 原有 tarball 参数保持;正常消费者验证之后通过共享公开 checkRelease 核对同一个归档,CI沿该入口必经硬门,不另打一个包代替原包。接线没有复制扫描实现。
237
+ 共享 `@hanamesh/release-platform-gate` **0.1.12** 已经 R119b 复审 PASS,来源 `hanamesh-docs/tools/release-platform-gate` 分支 `fix/pg1-leoventory-installer-risk-20261109`,提交 `95e54102f9380ea2e8e4d8a2e36eacd387c3f7e7`。正常工具 tgz SHA256 `caf78747649b1dd37cbe9ba331351c81f94a48dc9e779aee83c75336140068d1`;七载荷摘要、复审提交及冻结 SRI 见 `vendor/release-platform-gate.json`、`vendor/SHA256SUMS` 和锁。它仅为 devDependency,不进入运行时或产品 bundle。
234
238
 
235
- 有限依赖形态合同为正常开发工具包中的 `DEPENDENCY_SHAPES.md`(dependency-shapes-v1):支持 registry/git/file/link/tarball/workspace/bundled/vendor 的明确子集,不能当作 npm/pnpm 的无限全集。合同外一律 `UNSUPPORTED_DEPENDENCY_SHAPE`,逐项公开 `package/path/shape/why/action/contract`;缓存、bundle、环境变量或额外 CLI 开关都不能绕过准入。已知缺证据、冻结漂移、完整性和平台违规仍各自具名拒绝,不能以“任意失败”代替正确理由。
239
+ `DSH_PNPM_PLATFORM_CLOSURE` 是不可关闭的正确性不变量(disableable:false)。历史 DSH bundled pnpm 11.7.0 位于 pnpm#12297 风险系列,修复收录于 pnpm 11.23.0。原始 HanaMesh 挂起进程首错仍未找回,不能宣称历史身份/归因已闭合。合法 optional 平台字段和完整强 SRI 也不能独自证明安装器会退出。
236
240
 
237
- 发布说明:DSH 内置 pnpm 11.7.0 处理不适用平台 optional 子包时可能安装后不退出,使 DSH 一直等安装结束;因此本门是正确性不变量(`disableable:false`),没有关闭开关。所有 OS/CPU/libc 都必须具备平台中立的完整安装依赖闭包,不能用本机已装、omit-optional、ignore-scripts、超时、重试、放宽断言或改客户端代替。每次 PASS/FAIL 的 JSON/receipt 都包含触发背景和不能关闭的 reason;release:check 正常 pack,release:publish 只发布同一已检查且再核 SHA 的 tar。源码目录发布由 prepublishOnly 拒绝,CI 的 verify:pack/preflight 必经该门;无可失败 job 或旁路入口。
241
+ 根及必需路径的 os/cpu/libc 继续 PLATFORM_FIELD 拒绝;可选子图先完整核锁、实际归档、名称/版本/范围/peer、选择器一致与强 SRI,再按 `dsh-pnpm-platform-exit-v1` 核安装矩阵。所有平台工件均扫描,不按检查机过滤。缺矩阵为 INSTALLATION_TARGETS_REQUIRED,旧风险组合为 PNPM_PLATFORM_HANG_RISK,未审组合为 INSTALLATION_TARGET_UNSUPPORTED;混入一个安全目标不能洗绿旧组合。未知依赖形态仍 UNSUPPORTED_DEPENDENCY_SHAPE,输出 shape / why / action / contract;有限 dependency-shapes-v1 与 INSTALLATION_RISK.md 均随正常工具包交付。没有 skip/关闭开关、环境推断、重试、超时、omit-optional 或白名单绕过。
238
242
 
239
- 固定提供方 main 提交 `13ec0db3bc6aa6b6235f2105d3f0a6b998855ff5`,与 R2 审过的 0.1.10 正常包6载荷逐字一致;来源/归档摘要见 `vendor/release-platform-gate.json`。
243
+ 本次发布资格的完整目标明确保存在 `release/installation-targets.json`:DSH **0.2.2-rc.1 / pnpm 11.28.5 / darwin-arm64、linux-arm64**。这是 R119b 有退出证据的两个组合,不是检查机自动选择,也不把公开宽 peer 范围当作已通过的安装矩阵;其他宿主/安装器/架构不计入本次资格,旧组合仍拒绝。开发冻结安装的 pnpm 10.33.0 不是 bundled 11.28.5 的实装证据;后续 GT19 两机门必须核实际 pnpm 版本和完整闭包。
240
244
 
241
- ## Core 公开套件声明的测试依赖
245
+ `release:check` 与 `release:publish` 都显式传同一 `--installation-targets release/installation-targets.json`;`verify:pack` 在原消费者验证后通过共享公开 `checkRelease({installationTargets:...})` 检查同一归档,没有复制扫描实现或捕获忽略门错误。
242
246
 
243
- 源码测试通过 `@hanamesh/core/suite` 读取 Core 单源公开命令。`@hanamesh/core` 是仓库声明的 devDependency,按 npm 版本范围 `^0.3.28` 从公开 npm 安装,冻结 pnpm 锁记录实际版本与 SRI;许可证记录见 `docs/LICENSES.json`。Core 不成为运行时依赖或 bundled 包。
247
+ 运行 `npm run release:check` 产生正常归档并核验;`release:publish` 只发布同一已检查且复核 SHA 的 tar。`prepublishOnly` 的 source-guard 拒绝源码目录直接发布。CI 的既有 verify:pack / preflight 必经此门;无 continue-on-error 或旁路。此接线提交没有执行 npm publish、tag、部署或产品真机发布验收。
package/dist/client-ui.js CHANGED
@@ -129,15 +129,11 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
129
129
  "copy121": "固定规则(不能关)",
130
130
  "copy122": "为什么:",
131
131
  "copy123": "安装命令返回成功,但 profile 读回的版本与固定版本不一致",
132
- "copy124": "无法查询 DSH 配置的 registry;检查 DSH 源设置与网络,展开技术详情后手动重试",
133
- "registryLookupTimeout": "registry 查询超过 30 秒;检查 DSH 源设置与网络后手动重试",
134
- "registryConfigUnavailable": "DSH 未提供可读的源配置;检查客户端插件管理器,展开技术详情",
135
132
  "copy125": "DSH 安装命令失败,请展开技术详情查看原始原因",
136
- "installAgeRejected": "宿主的依赖发布时间检查未通过(默认需发布满 24 小时,以技术详情中的 cutoff 为准)。已知 DSH 0.2.0-rc.2 内置 pnpm 11.7.0 对同名多条白名单只认第一条。请在 DSH 设置中检查当前版本并升级 DSH,或等待受影响版本满足规则后手动重试。",
133
+ "installAgeRejected": "宿主的依赖发布时间检查未通过。请核对安装目标包含精确版本并保留技术详情;若仍失败,请在 DSH 设置中检查并升级 DSH。",
137
134
  "installAffectedPackages": "受影响包:",
138
135
  "copy126": "当前宿主未提供安装参数(profile / node 路径)",
139
136
  "copy127": "目录里已没有这一项,刷新后重试",
140
- "copy128": "registry 上的最新版是预发布版(rc),客户端未允许安装预发布版",
141
137
  "copy129": "运行时归档下载失败,检查网络或来源",
142
138
  "copy130": "10 分钟内没有结果,可稍后重试",
143
139
  "copy131": "HanaMesh 套件自身不能从市场安装或卸载",
@@ -271,8 +267,8 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
271
267
  "copy261": "准备安装环境未完成。请查看技术详情中的准确原因;未自动换源或重试。",
272
268
  "copy262": "只在本机 127.0.0.1 上监听,请求的 Host 必须是 127.0.0.1 或 localhost 加监听端口",
273
269
  "copy263": "防止别的机器或 DNS 重绑定的网页冒充本机",
274
- "copy264": "只有「允许交接的网站」里的网站能探测本客户端、交接安装、读交接状态;别的网站什么也读不到",
275
- "copy265": "否则任何网页都能知道你装了什么客户端、缺什么,用来识别你",
270
+ "copy264": "只有「允许交接的网站」能读取客户端能力、交接安装和交接状态;未许可网站仅能读取固定的来源拒绝提示",
271
+ "copy265": "拒绝提示只说明本机监听拒绝了该网站,不透露客户端类型、能力、安装情况或允许名单",
276
272
  "copy266": "网站交接只会在应用库里弹出确认;只有你点「确认安装」才会安装",
277
273
  "copy267": "网站不能替你决定装什么",
278
274
  "copy268": "交接状态只给发起它的那个网站看",
@@ -434,15 +430,11 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
434
430
  "copy121": "Required rules",
435
431
  "copy122": "Reason: ",
436
432
  "copy123": "Installation command succeeded, but the version read from the profile differs from the confirmed version",
437
- "copy124": "Cannot query the registry configured in DSH. Check DSH source settings and network, expand technical details, then retry manually",
438
- "registryLookupTimeout": "Registry lookup exceeded 30 seconds. Check DSH source settings and network, then retry manually",
439
- "registryConfigUnavailable": "DSH did not expose readable registry configuration. Check the client plugin manager and expand technical details",
440
433
  "copy125": "DSH installation failed. Expand Technical details for the original cause",
441
- "installAgeRejected": "The host rejected a dependency's release age (the default requires 24 hours since publication; see the actual cutoff in Technical details). Known issue: pnpm 11.7.0 bundled with DSH 0.2.0-rc.2 only recognizes the first allowlist entry for a package name. Check your version in DSH Settings and upgrade DSH, or wait until the affected versions meet the rule before trying again manually.",
434
+ "installAgeRejected": "The host rejected a dependency release-age check. Verify the exact installation target and retain Technical details. If it still fails, check and upgrade DSH in Settings.",
442
435
  "installAffectedPackages": "Affected packages: ",
443
436
  "copy126": "This host has not provided installation parameters (profile / Node path)",
444
437
  "copy127": "This item is no longer in the catalog. Refresh before trying again",
445
- "copy128": "The latest registry version is a prerelease, which this client has not allowed",
446
438
  "copy129": "Runtime archive download failed. Check the network or source",
447
439
  "copy130": "No result within 10 minutes. Check the installed state before trying again",
448
440
  "copy131": "The HanaMesh suite cannot be installed or uninstalled through the market",
@@ -576,8 +568,8 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
576
568
  "copy261": "Installation environment preparation did not complete. See Technical details for the exact reason. No automatic source switching or retry occurred.",
577
569
  "copy262": "Listen only on 127.0.0.1. The request Host must be 127.0.0.1 or localhost with the listening port.",
578
570
  "copy263": "Prevents remote machines or DNS-rebinding pages from impersonating this machine.",
579
- "copy264": "Only allowed websites can discover this client, hand off installation or read handoff status.",
580
- "copy265": "Prevents unrelated websites from identifying you through installed clients or missing capabilities.",
571
+ "copy264": "Only allowed websites can read client capabilities, hand off installations or read handoff status. Other websites can read only a fixed origin-refusal message.",
572
+ "copy265": "The refusal reveals only that a local listener rejected the website, without client type, capabilities, installations or the allowlist.",
581
573
  "copy266": "Website handoffs only open market confirmation. Installation requires your Confirm installation action.",
582
574
  "copy267": "A website cannot decide what to install for you.",
583
575
  "copy268": "Handoff status is visible only to the website that created it.",
@@ -841,12 +833,12 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
841
833
  defaults&&names?h('div',null,h('h3',null,t('设置名称与默认值')),h('dl',null,...['enabled','ports','allowedOrigins','ttlMs','pollMs'].flatMap(key=>[h('dt',{key:key+'-name'},names[key]),h('dd',{key:key+'-value'},Array.isArray(defaults[key])?defaults[key].join(', '):typeof defaults[key]==='boolean'?(defaults[key]?t('开'):t('关')):String(defaults[key]))]))):null,
842
834
  value?.rules?h('div',null,h('h3',null,t('固定规则(不能关)')),h('ul',null,...value.rules.map(row=>h('li',{key:row.rule},t(row.rule),h('br',null),h('small',null,t('为什么:')+t(row.why)))))):null));
843
835
  }
844
- const FAILURE_REASONS={...CORE_SUITE_FAILURES,INSTALL_READBACK_MISMATCH:t('安装命令返回成功,但 profile 读回的版本与固定版本不一致'),REGISTRY_LOOKUP_FAILED:t('无法查询 DSH 配置的 registry;检查 DSH 源设置与网络,展开技术详情后手动重试'),REGISTRY_LOOKUP_TIMEOUT:t('registry 查询超过 30 秒;检查 DSH 源设置与网络后手动重试'),REGISTRY_CONFIG_UNAVAILABLE:t('DSH 未提供可读的源配置;检查客户端插件管理器,展开技术详情'),DSH_PLUGIN_FAILED:t('DSH 安装命令失败,请展开技术详情查看原始原因'),LIBRARY_INSTALL_UNAVAILABLE:t('当前宿主未提供安装参数(profile / node 路径)'),CATALOG_ITEM_MISSING:t('目录里已没有这一项,刷新后重试'),UNSTABLE_VERSION:t('registry 上的最新版是预发布版(rc),客户端未允许安装预发布版'),RUNTIME_ARCHIVE_UNAVAILABLE:t('运行时归档下载失败,检查网络或来源'),OPERATION_TIMEOUT:t('10 分钟内没有结果,可稍后重试'),PACKAGE_DENIED:t('HanaMesh 套件自身不能从市场安装或卸载'),INSTANCE_IN_USE:t('先关闭该应用的所有视图再卸载')};
836
+ const FAILURE_REASONS={...CORE_SUITE_FAILURES,INSTALL_READBACK_MISMATCH:t('安装命令返回成功,但 profile 读回的版本与固定版本不一致'),DSH_PLUGIN_FAILED:t('DSH 安装命令失败,请展开技术详情查看原始原因'),LIBRARY_INSTALL_UNAVAILABLE:t('当前宿主未提供安装参数(profile / node 路径)'),CATALOG_ITEM_MISSING:t('目录里已没有这一项,刷新后重试'),RUNTIME_ARCHIVE_UNAVAILABLE:t('运行时归档下载失败,检查网络或来源'),OPERATION_TIMEOUT:t('10 分钟内没有结果,可稍后重试'),PACKAGE_DENIED:t('HanaMesh 套件自身不能从市场安装或卸载'),INSTANCE_IN_USE:t('先关闭该应用的所有视图再卸载')};
845
837
  const failureReason=(code,details)=>{
846
838
  const diagnostic=code==='DSH_PLUGIN_FAILED'?details?.diagnostic??'':'',pnpmCode=diagnostic.match(/\bERR_PNPM_[A-Z0-9_]+\b/)?.[0];
847
839
  if(pnpmCode==='ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION'){
848
840
  const packages=[...new Set([...diagnostic.matchAll(/((?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\s+was published\b/g)].map(match=>match[1]))];
849
- return pnpmCode+' · '+t('宿主的依赖发布时间检查未通过(默认需发布满 24 小时,以技术详情中的 cutoff 为准)。已知 DSH 0.2.0-rc.2 内置 pnpm 11.7.0 对同名多条白名单只认第一条。请在 DSH 设置中检查当前版本并升级 DSH,或等待受影响版本满足规则后手动重试。')+(packages.length?' '+t('受影响包:')+packages.join(', '):'');
841
+ return pnpmCode+' · '+t('宿主的依赖发布时间检查未通过。请核对安装目标包含精确版本并保留技术详情;若仍失败,请在 DSH 设置中检查并升级 DSH。')+(packages.length?' '+t('受影响包:')+packages.join(', '):'');
850
842
  }
851
843
  return(pnpmCode?pnpmCode+' · ':'')+(t(FAILURE_REASONS[code])??t('详细原因见宿主日志'));
852
844
  };
@@ -1,6 +1,6 @@
1
1
  import semver from 'semver';
2
2
 
3
3
  /** Public host compatibility; exact dependency versions in devDependencies remain reproducible test inputs. */
4
- export const DSH_RANGE='>=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0';
4
+ export const DSH_RANGE='>=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0';
5
5
  /** saveExact entered the public PluginManager contract in 0.2.1-alpha.2. Unknown versions fail closed. */
6
6
  export function managerSupportsExactSave(version){return typeof version==='string'&&semver.satisfies(version,'>=0.2.1-alpha.2 <0.3.0-0',{includePrerelease:true});}
package/dist/dsh.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type {InstallationReaderConfig} from './installation-status.js';
2
1
  import type { AppHost, AppDefinition, AuthenticatedSubject, Awaitable, DshStorageBinding } from './index.js';
3
2
  export const name: 'hanamesh-app-host';
4
3
  export const DSH_TARGET: '>=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0';
@@ -13,7 +12,7 @@ export interface DshPluginConfig {
13
12
  /** `sources` undefined → the HanaMesh catalog source; explicit `[]` → none. `profileDir`/`profileName`/`dshBin` (and
14
13
  * `nodeBinary`) are inferred from the proven Desktop layout or plain DSH when undefined — see docs/CONTRACT.md 「应用库配置」; explicit values win. */
15
14
  /** rc.28: the same keys serve the HanaMesh market (plugins + applications; routes `/hanamesh/library*`, see docs/CONTRACT.md「市场」). */
16
- library?:{fixture?:string;sources?:Array<{manifestUrl:string;enabled?:boolean}>;profileDir?:string;profileName?:string;dshBin?:string;registry?:string;allowPrerelease?:boolean;eventRetention?:number;installationReaders?:Partial<Record<'claude'|'codex',InstallationReaderConfig>>};
15
+ library?:{fixture?:string;sources?:Array<{manifestUrl:string;enabled?:boolean}>;profileDir?:string;profileName?:string;dshBin?:string;registry?:string;eventRetention?:number};
17
16
  applications?: AppDefinition[]; leaseTtlMs?: number; sweepIntervalMs?: number;
18
17
  }
19
18
  /** Adapt an open `ctx.storageDomain` handle (single layout, global snapshot) to the store binding. */
package/dist/dsh.js CHANGED
@@ -73,11 +73,9 @@ export const Config = z.object({
73
73
  sources:z.union([z.array(z.any()),z.const(undefined)]),
74
74
  /** Locations are inferred from the verified Desktop argv/profile or plain DSH (see library/locate.js). */
75
75
  profileDir:z.string(),profileName:z.string(),dshBin:z.string(),
76
- registry:z.string(),allowPrerelease:z.boolean(),
76
+ registry:z.string(),
77
77
  /** Publicly named event history capacity; a lost cursor reports resetRequired. */
78
78
  eventRetention:z.number().default(512),
79
- /** Explicit official CLI readers; unset means unknown, never inferred from PATH or private files. */
80
- installationReaders:z.union([z.any(),z.const(undefined)]),
81
79
  /** Website → client handoff listener (library/handoff.js). Unset fields use the defaults shown in the library
82
80
  * settings page, where they can also be changed (saved values win over this config). */
83
81
  handoff:z.object({enabled:z.boolean(),ports:z.union([z.array(z.number()),z.const(undefined)]),
@@ -236,8 +234,6 @@ export async function apply(ctx, config) {
236
234
  locations.inferred.map(key=>[key,key==='sources'?locations.sources.map(source=>source.manifestUrl):locations[key]]))));
237
235
  // Exact-save capability comes from the host's public package metadata. Older managers use the public CLI
238
236
  // for installs and preserve restart-required; applications and compensation also use that CLI.
239
- let pluginManager;
240
- optional('pluginManager', service => { pluginManager=service; });
241
237
  let dshBin=locations.dshBin;
242
238
  if(!dshBin&&locations.profileDir&&!desktop)try{dshBin=createRequire(join(locations.profileDir,'package.json')).resolve('@deepseek-ai/dsh/lib/bin.js');}catch{dshBin=undefined;}
243
239
  const cliUsable=Boolean(locations.desktopLauncher||dshBin&&(!desktop||locations.desktopCli||config.library?.dshBin));
@@ -245,9 +241,9 @@ export async function apply(ctx, config) {
245
241
  if(locations.profileDir&&locations.profileName){
246
242
  provisionApi=await import('@hanamesh/lib-provision');
247
243
  installer=nodeBinding(()=>nodeRuntime.executable,nodeBinary=>createLibraryInstaller({profileDir:locations.profileDir,profileName:locations.profileName,dataRoot,
248
- ...(cliUsable?{nodeBinary,dshBin,desktopCli:locations.desktopCli,desktopLauncher:locations.desktopLauncher}:{}),pluginManagerVersion:locations.pluginManagerVersion,profileWriter,registryRules:locations.pluginManagerRegistry?()=>import(pathToFileURL(locations.pluginManagerRegistry).href):undefined,
249
- registry:libraryConfig.registry,allowPrerelease:libraryConfig.allowPrerelease,provision:provisionApi.provision,remove:provisionApi.remove,ledger:provisionApi.ledger,
250
- coreService:()=>coreService,suiteSourceService:()=>suiteSourceService,suiteSourceMode:()=>library?.suiteSourceSettings().mode??'configured',pluginManager:()=>pluginManager,emit:event=>library?.emit(event)}));
244
+ ...(cliUsable?{nodeBinary,dshBin,desktopCli:locations.desktopCli,desktopLauncher:locations.desktopLauncher}:{}),pluginManagerVersion:locations.pluginManagerVersion,profileWriter,
245
+ registry:libraryConfig.registry,provision:provisionApi.provision,remove:provisionApi.remove,ledger:provisionApi.ledger,
246
+ coreService:()=>coreService,suiteSourceService:()=>suiteSourceService,suiteSourceMode:()=>library?.suiteSourceSettings().mode??'configured',emit:event=>library?.emit(event)}));
251
247
  }
252
248
  /** What a person must do so the library can install, in their words (C-MARKET probe `error.need`). */
253
249
  const unavailable=()=>{
@@ -262,7 +258,7 @@ export async function apply(ctx, config) {
262
258
  };
263
259
  const identity=await detectClient({desktop});
264
260
  if(typeof ctx.logger?.info==='function')ctx.logger.info('hanamesh-app-host library: client %s (%s)',identity.client,identity.evidence);
265
- library=createLibraryService({domain:{global:libraryDomain.global,close:closeLibraryDomain},host,config:libraryConfig,dataRoot,ledgerReader:provisionApi?.ledger??(async()=>({schema:1,items:{}})),installer,client:identity.client,unavailable,suiteSourceService:()=>suiteSourceService,installationStatus:nodeStatusBinding(()=>nodeRuntime.executable,workerNode=>createInstallationStatusReader({client:identity.client,core:()=>coreService,workerNode,readers:libraryConfig.installationReaders})),logger:ctx.logger});
261
+ library=createLibraryService({domain:{global:libraryDomain.global,close:closeLibraryDomain},host,config:libraryConfig,dataRoot,ledgerReader:provisionApi?.ledger??(async()=>({schema:1,items:{}})),installer,client:identity.client,unavailable,suiteSourceService:()=>suiteSourceService,installationStatus:nodeStatusBinding(()=>nodeRuntime.executable,workerNode=>createInstallationStatusReader({client:identity.client,core:()=>coreService,workerNode})),logger:ctx.logger});
266
262
  own('library', () => library.close());
267
263
  await library.init();
268
264
  // rc.27: app bundles register plain `app.json` (no package name); the installed scan knows which npm package ships
@@ -0,0 +1,12 @@
1
+ // Live Windows tree anchor; its reaper closes the tree before removing trace files.
2
+ import {spawn,spawnSync} from 'node:child_process';
3
+ import {join} from 'node:path';
4
+ const stop=()=>{spawnSync(join(process.env.SystemRoot,'System32','taskkill.exe'),['/PID',String(process.pid),'/T','/F'],{stdio:'ignore',windowsHide:true});process.exit(1);};
5
+ process.once('disconnect',stop);
6
+ process.once('message',({command,args,options})=>{
7
+ const {input,...spawnOptions}=options;
8
+ const child=spawn(command,args,{...spawnOptions,stdio:[input===undefined?'ignore':'pipe','inherit','inherit'],detached:false});
9
+ if(input!==undefined){child.stdin.on('error',()=>{});child.stdin.end(input);}
10
+ child.once('error',error=>{if(process.connected)process.send({error:{code:error.code??'SPAWN_FAILED'}});});
11
+ child.once('close',(code,signal)=>{if(process.connected)process.send({result:{code,status:code,signal}});});
12
+ });
@@ -7,6 +7,11 @@ const policy=Object.freeze({GIT_CONFIG_NOSYSTEM:'1',GIT_TERMINAL_PROMPT:'0',GIT_
7
7
  GIT_CONFIG_KEY_1:'credential.helper',GIT_CONFIG_VALUE_1:'!gh auth git-credential'});
8
8
  export function safeGitEnv(base=process.env){
9
9
  const env={...base};
10
+ // Nested guards share ownership only within the same explicitly selected temp root.
11
+ // A caller choosing a different scratch root starts an independent trace owner.
12
+ if(!env.HM_GIT_GUARD_PARENT_ROOT&&base.TMPDIR===process.env.TMPDIR&&process.env.HM_GIT_GUARD_PARENT_ROOT){
13
+ env.HM_GIT_GUARD_PARENT_ROOT=process.env.HM_GIT_GUARD_PARENT_ROOT;env.HM_GIT_GUARD_TMP_ROOT=process.env.HM_GIT_GUARD_TMP_ROOT;
14
+ }
10
15
  for(const key of Object.keys(env))if(/^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+|PARAMETERS)$/.test(key)||/^GIT_TRACE/.test(key)||['GIT_ASKPASS','SSH_ASKPASS'].includes(key))delete env[key];
11
16
  return {...env,...policy,npm_config_manage_package_manager_versions:'false',pnpm_config_manage_package_manager_versions:'false'};
12
17
  }
@@ -27,7 +32,7 @@ export function observeGitTrace(directory){
27
32
  let event;try{event=JSON.parse(line);}catch{throw Object.assign(new Error('Invalid Git credential trace.'),{code:'GIT_TRACE_INVALID'});}
28
33
  if(['child_start','exec'].includes(event.event)&&forbidden.test((event.argv??[]).join(' '))){failure=Object.assign(new Error('Forbidden macOS keychain helper was invoked; operation failed.'),{code:'GIT_KEYCHAIN_FORBIDDEN'});throw failure;}
29
34
  }
30
- },close(){if(closed)return;closed=true;try{closeSync(fd);}finally{rmSync(directory,{recursive:true,force:true});}}};
35
+ },close(){if(closed)return;closed=true;try{closeSync(fd);}finally{rmSync(directory,{recursive:true,force:true,maxRetries:4,retryDelay:25});}}};
31
36
  }
32
37
  export function guardNodeBinary({workerNode,versions=process.versions,execPath=process.execPath}={}){
33
38
  if(workerNode!==undefined)return typeof workerNode==='string'&&isAbsolute(workerNode)?workerNode:undefined;
@@ -43,7 +48,7 @@ export async function spawnGitSafe(command,args,options={}){
43
48
  try{
44
49
  if(signal?.aborted)throw Object.assign(new Error('Guarded command aborted.'),{name:'AbortError',code:'ABORT_ERR'});
45
50
  const stdio=options.stdio==='inherit'?['ignore','inherit','inherit','ipc']:['ignore','pipe','pipe','ipc'];
46
- worker=fork(new URL('./git-worker.js',import.meta.url),[],{cwd:options.cwd,env:safeGitEnv(options.env??process.env),execArgv:[],execPath:nodeRuntime,detached:process.platform!=='win32',stdio});
51
+ worker=fork(new URL('./git-worker.js',import.meta.url),[],{cwd:options.cwd,env:safeGitEnv(options.env??process.env),execArgv:[],execPath:nodeRuntime,detached:true,windowsHide:true,stdio});
47
52
  worker.stdout?.on('data',chunk=>{stdout=(stdout+chunk.toString(encoding)).slice(-4*1024*1024);});
48
53
  worker.stderr?.on('data',chunk=>{stderr=(stderr+chunk.toString(encoding)).slice(-4*1024*1024);});
49
54
  signal?.addEventListener('abort',abort,{once:true});
@@ -59,6 +64,6 @@ export async function spawnGitSafe(command,args,options={}){
59
64
  }finally{
60
65
  clearTimeout(deadline);clearTimeout(abortFallback);signal?.removeEventListener('abort',abort);
61
66
  if(worker?.connected)worker.disconnect();
62
- if(directory)rmSync(directory,{recursive:true,force:true});
67
+ if(directory)rmSync(directory,{recursive:true,force:true,maxRetries:4,retryDelay:25});
63
68
  }
64
69
  }
@@ -1,16 +1,32 @@
1
1
  // Internal IPC supervisor. The parent never detaches a package manager directly.
2
- import {spawn,spawnSync} from 'node:child_process';
2
+ import {spawn,spawnSync,fork} from 'node:child_process';
3
3
  import {existsSync,mkdtempSync,writeFileSync,rmSync,realpathSync} from 'node:fs';
4
4
  import {tmpdir} from 'node:os';
5
5
  import {dirname,join,basename} from 'node:path';
6
+ import {createWindowsJob} from './windows-job.js';
6
7
  import {observeGitTrace} from './git-process.js';
7
- let monitor,directory,timer,done=false,started=false;
8
- function finish(payload){
8
+ let monitor,directory,timer,done=false,started=false,anchor,anchorClosed,windowsJob,jobStarting,jobController;
9
+ async function finish(payload){
9
10
  if(done)return;done=true;clearInterval(timer);
11
+ if(process.platform==='win32'&&anchor){
12
+ try{
13
+ if(!windowsJob){jobController?.abort();try{await jobStarting}catch{}}
14
+ if(windowsJob)await windowsJob.stop();else if(anchor.exitCode===null&&anchor.signalCode===null)anchor.kill('SIGKILL');
15
+ await anchorClosed;
16
+ }catch{payload={error:{code:'GIT_TREE_CLEANUP_FAILED'}};}
17
+ }
18
+ finalize(payload);
19
+ }
20
+ async function commandResult(payload){
21
+ if(done)return;
22
+ if(!payload.error){try{while(!done&&await windowsJob.active()>1)await new Promise(resolve=>setTimeout(resolve,5));}catch{void finish({error:{code:'GIT_TREE_CLEANUP_FAILED'}});return;}}
23
+ if(!done)void finish(payload);
24
+ }
25
+ function finalize(payload){
10
26
  try{monitor?.check(true);}catch(error){payload={error:{code:error.code??'GIT_TRACE_INVALID'}};}
11
27
  try{if(monitor)monitor.close();else if(directory)rmSync(directory,{recursive:true,force:true});}catch{payload={error:{code:'GIT_TRACE_CLEANUP_FAILED'}};}
12
28
  const end=()=>{
13
- if(process.platform==='win32'){spawnSync('taskkill',['/pid',String(process.pid),'/T','/F'],{stdio:'ignore'});process.exit(1);}
29
+ if(process.platform==='win32')process.exit(0);
14
30
  else process.kill(-process.pid,'SIGKILL');
15
31
  };
16
32
  if(process.connected)process.send(payload,end);else end();
@@ -19,15 +35,17 @@ function abort(code='ABORT_ERR'){finish({error:{code,...(code==='ABORT_ERR'?{nam
19
35
  process.once('disconnect',()=>abort());
20
36
  process.once('SIGTERM',()=>abort());process.once('SIGINT',()=>abort());
21
37
  if(!process.connected)process.exit(1);
22
- process.on('message',request=>{
38
+ process.on('message',async request=>{
23
39
  if(request.abort)return abort();if(request.timeout)return abort('ETIMEDOUT');
24
40
  if(started)return;started=true;
25
41
  try{
26
42
  let {command,args,options}=request;
27
43
  // No caller-owned trace exists before this IPC worker can clean on disconnect.
28
- directory=mkdtempSync(join(realpathSync(options.env.TMPDIR??tmpdir()),'hm-git-guard-'));
44
+ const temporaryRoot=realpathSync(options.env.TMPDIR??tmpdir());
45
+ const parentRoot=options.env.HM_GIT_GUARD_TMP_ROOT===temporaryRoot&&options.env.HM_GIT_GUARD_PARENT_ROOT?realpathSync(options.env.HM_GIT_GUARD_PARENT_ROOT):temporaryRoot;
46
+ directory=mkdtempSync(join(parentRoot,'hm-git-guard-'));
29
47
  writeFileSync(join(directory,'events.jsonl'),'',{mode:0o600});monitor=observeGitTrace(directory);
30
- options.env={...options.env,GIT_TRACE2_EVENT:join(directory,'events.jsonl'),GIT_TRACE_REDACT:'1'};
48
+ options.env={...options.env,HM_GIT_GUARD_PARENT_ROOT:directory,HM_GIT_GUARD_TMP_ROOT:temporaryRoot,GIT_TRACE2_EVENT:join(directory,'events.jsonl'),GIT_TRACE_REDACT:'1'};
31
49
  if(!process.connected)return abort();
32
50
  process.send({directory},error=>{if(error)abort();});
33
51
  // Native Windows .cmd shims cannot be executed by spawn without a shell.
@@ -40,6 +58,18 @@ process.on('message',request=>{
40
58
  const cli=candidates.find(path=>existsSync(path));if(!cli)throw Object.assign(new Error('Package manager JS entry unavailable.'),{code:'PACKAGE_MANAGER_UNAVAILABLE'});
41
59
  command=process.execPath;args=[cli,...args];
42
60
  }
61
+ if(process.platform==='win32'){
62
+ anchor=fork(new URL('./git-command-worker.js',import.meta.url),[],{cwd:options.cwd,env:options.env,execArgv:[],stdio:['ignore','inherit','inherit','ipc']});
63
+ anchorClosed=new Promise(resolve=>anchor.once('close',resolve));
64
+ anchor.once('message',value=>{void commandResult(value)});anchor.once('error',error=>{void finish({error:{code:error.code??'SPAWN_FAILED'}})});
65
+ jobController=new AbortController();
66
+ jobStarting=createWindowsJob({pid:anchor.pid,env:{...options.env,TMPDIR:directory,TMP:directory,TEMP:directory},cwd:options.cwd,signal:jobController.signal});
67
+ windowsJob=await jobStarting;
68
+ if(done)return;
69
+ anchor.send({command,args,options});
70
+ timer=setInterval(()=>{try{monitor.check();}catch(error){finish({error:{code:error.code??'GIT_TRACE_INVALID'}});}},25);
71
+ return;
72
+ }
43
73
  const {input,...spawnOptions}=options;
44
74
  const child=spawn(command,args,{...spawnOptions,stdio:[input===undefined?'ignore':'pipe','inherit','inherit'],detached:false});
45
75
  if(input!==undefined){child.stdin.on('error',()=>{});child.stdin.end(input);}
@@ -5,7 +5,7 @@
5
5
  * real provider. Fixture data is a test catalog and is labelled as such in the market UI.
6
6
  */
7
7
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
8
- import { join, dirname } from 'node:path';
8
+ import { join, dirname, resolve } from 'node:path';
9
9
  import { createServer, request } from 'node:http';
10
10
  import { randomUUID } from 'node:crypto';
11
11
  import { fileURLToPath } from 'node:url';
@@ -94,8 +94,8 @@ export async function sourceRecoveryProfile({recovery},item={}){
94
94
  if(item.mode==='upgrade')await profile.seed(target.packageName,app.previous);
95
95
  for(const [command,phase,skip] of item.inject??[])profile.fail(command,phase,skip);
96
96
  if(item.concurrent)profile.concurrentAdd('@fixture/concurrent','1.0.0');
97
- // Named single-source contract fixture; not production registry policy.
98
- const installer=emit=>createLibraryInstaller({registryRules:async()=>({registryPlan:(_requested,configured)=>[configured.registry],attributeFailure:()=>'other'}),pluginManager:()=>({registries:async()=>({registry:'https://source-fixture.invalid/',fallbackRegistries:[],resolved:'https://source-fixture.invalid/'})}),profileDir:profile.profileDir,profileName:profile.profileName,dataRoot:profile.dataRoot,nodeBinary:'/source-fixture/bin/node',dshBin:'/source-fixture/dsh/lib/bin.js',
97
+ // The catalog fixture already supplies the exact reviewed version.
98
+ const installer=emit=>createLibraryInstaller({profileDir:profile.profileDir,profileName:profile.profileName,dataRoot:profile.dataRoot,nodeBinary:resolve('/source-fixture/bin/node'),dshBin:resolve('/source-fixture/dsh/lib/bin.js'),
99
99
  fetchImpl:profile.fetchImpl({[app.packageName]:app.latest,[plugin.packageName]:plugin.latest}),spawn:profile.spawn,provision:profile.provision,remove:profile.remove,ledger:profile.ledger,emit});
100
100
  return{profile,target,installer};
101
101
  }
@@ -1,13 +1,5 @@
1
1
  /** Advertised metadata only. Discovery performs no installation read. */
2
2
  export const INSTALLATION_STATUS_CAPABILITY:Readonly<{contract:'hanamesh.installation-status';contractVersion:'1';path:'/hanamesh/library/installation-status'}>;
3
- export interface InstallationReaderConfig {
4
- /** Absolute official executable and explicitly selected user/config directories. Never supplied by a website. */
5
- executable:string;home:string;configDir:string;
6
- /** Identity and exact HTTPS marketplace URL from the registered AgentPluginDTO. */
7
- pluginName:string;marketplace:string;marketplaceUrl:string;
8
- /** Required positive bounded operation budgets. No product default is inferred. */
9
- timeoutMs:number;maxOutputBytes:number;
10
- }
11
3
  export interface InstallationStatusRequest {contractVersion:'1';consent:'read-installations'}
12
4
  export interface InstallationObservation {
13
5
  channel:'hanamesh'|'dsh'|'claude'|'codex';
@@ -27,7 +27,6 @@ export function installationDiagnostic(error){
27
27
  cause=field(cause,'cause');
28
28
  }
29
29
  // Only known installer fields, never arbitrary details/stack/response objects.
30
- if(error instanceof AppHostError&&error.code==='REGISTRY_LOOKUP_FAILED'&&Number.isInteger(error.details?.status))lines.push(`Registry HTTP status: ${error.details.status}`);
31
30
  if(error instanceof AppHostError&&error.code==='DSH_PLUGIN_FAILED'){
32
31
  lines.push(`CLI exit: ${Number.isInteger(error.details?.code)?error.details.code:'unknown'}; signal: ${clean(error.details?.signal,80)||'none'}`);
33
32
  lines.push(`stderr: ${clean(error.details?.stderr)||'<empty>'}`);
@@ -34,7 +34,7 @@ export function packageInstallTarget(value){
34
34
 
35
35
  export const installTargetName=target=>target.source==='github'?target.packageName:target.name;
36
36
  export const sameInstallTarget=(left,right)=>JSON.stringify(packageInstallTarget(left))===JSON.stringify(packageInstallTarget(right));
37
- export const catalogInstallationAllowed=item=>item?.installMethods===undefined?item?.oneClickInstall?.allowed!==false:item?.oneClickInstall?.allowed===true;
37
+ export const catalogInstallationAllowed=item=>Array.isArray(item?.installMethods)&&item?.oneClickInstall?.allowed===true;
38
38
  export function catalogPackageTargets(item){
39
39
  if(!catalogInstallationAllowed(item))return[];
40
40
  return (item.installMethods??[]).filter(method=>['hanamesh','dsh'].includes(method.channel)&&method.target)
@@ -1,4 +1,5 @@
1
1
  import { installationStatusCors, serveInstallationStatus } from './installation-status-http.js';
2
+ import {serveInstallTargetProbe} from './install-target-http.js';
2
3
  /**
3
4
  * Website → client install handoff (C-MARKET, 2026-10-15 E/F). Upstream Desktop listens on an OS-assigned port that
4
5
  * nothing records, and a browser tab without the launch token gets 401, so a website can neither find nor enter the
@@ -33,7 +34,7 @@ const TIMER_MAX_MS = 2_147_483_647;
33
34
  /** Fixed rules (correctness, not policy): listed on the settings page with the reason they cannot be switched off. */
34
35
  export const HANDOFF_RULES = Object.freeze([
35
36
  { rule: '只在本机 127.0.0.1 上监听,请求的 Host 必须是 127.0.0.1 或 localhost 加监听端口', why: '防止别的机器或 DNS 重绑定的网页冒充本机' },
36
- { rule: '只有「允许交接的网站」里的网站能探测本客户端、交接安装、读交接状态;别的网站什么也读不到', why: '否则任何网页都能知道你装了什么客户端、缺什么,用来识别你' },
37
+ { rule: '只有「允许交接的网站」能读取客户端能力、交接安装和交接状态;未许可网站仅能读取固定的来源拒绝提示', why: '拒绝提示只说明本机监听拒绝了该网站,不透露客户端类型、能力、安装情况或允许名单' },
37
38
  { rule: '网站交接只会在应用库里弹出确认;只有你点「确认安装」才会安装', why: '网站不能替你决定装什么' },
38
39
  { rule: '交接状态只给发起它的那个网站看', why: '一个网站不能读另一个网站的交接' },
39
40
  { rule: `交接请求体不超过 ${HANDOFF_BODY_MAX_BYTES} 字节`, why: '交接只包含目录身份、合约版本及已审核的结构化安装目标;正文上限为 4096 字节,不接受来源命令' },
@@ -168,16 +169,7 @@ export function createHandoffHandler({ library, queue, settings, port }) {
168
169
  requireCondition([`127.0.0.1:${bound}`, `localhost:${bound}`].includes(req.headers.host), 'HOST_DENIED', 'Unrecognized request host.', {}, 403);
169
170
  const url = new URL(req.url ?? '/', `http://${req.headers.host}`), path = url.pathname, origin = req.headers.origin;
170
171
  if (path === '/hanamesh/library/install-target') {
171
- // A website not in allowedOrigins gets no CORS grant and no body: it cannot tell which client runs or what it lacks.
172
- // Requests without an Origin are not from a website (a person's address bar, a local tool) and are answered.
173
- if (origin !== undefined) {
174
- requireCondition(settings().allowedOrigins.includes(origin), 'ORIGIN_DENIED', 'Not available.', undefined, 403);
175
- cors = { 'access-control-allow-origin': origin, vary: 'Origin', 'access-control-allow-private-network': 'true' };
176
- }
177
- if (req.method === 'OPTIONS') { send(res, 204, undefined, { ...cors, 'access-control-allow-methods': 'GET' }); return; }
178
- requireCondition(req.method === 'GET', 'METHOD_NOT_ALLOWED', 'Discovery requires GET.', {}, 405);
179
- requireCondition(!url.search, 'UNKNOWN_FIELDS', 'Discovery takes no parameters.');
180
- send(res, 200, { ...library.installTarget(), traceId }, cors); return;
172
+ serveInstallTargetProbe(req,res,{allowedOrigins:settings().allowedOrigins,read:()=>library.installTarget(),traceId,send});return;
181
173
  }
182
174
  if(path === '/hanamesh/library/installation-status'){
183
175
  cors=installationStatusCors(req,settings().allowedOrigins);
@@ -0,0 +1,39 @@
1
+ /** Public discovery only: an unapproved website may read one constant refusal, never client facts. */
2
+ import {AppHostError,requireCondition} from '../errors.js';
3
+ const PATH='/hanamesh/library/install-target';
4
+ const VARY='Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network';
5
+ function browserOrigin(origin){
6
+ if(typeof origin!=='string')return false;
7
+ try{const url=new URL(origin);return ['https:','http:'].includes(url.protocol)&&url.origin===origin}catch{return false}
8
+ }
9
+ export function serveInstallTargetProbe(req,res,{allowedOrigins,read,traceId,send}){
10
+ let cors={};
11
+ try{
12
+ requireCondition(req.url?.startsWith('/')&&!req.url.startsWith('//')&&!req.url.includes('\\')&&req.url.split('?')[0]===PATH,'BAD_TARGET','Invalid discovery target.');
13
+ requireCondition(req.headers['sec-fetch-dest']!=='iframe','FRAME_CONTROL_DENIED','An application frame cannot inspect the host.',{},403);
14
+ const url=new URL(req.url,'http://127.0.0.1'),origin=req.headers.origin,approved=origin===undefined||allowedOrigins.includes(origin);
15
+ if(!approved){
16
+ requireCondition(browserOrigin(origin),'ORIGIN_DENIED','Not available.',{},403);
17
+ requireCondition(req.method==='GET'||req.method==='OPTIONS','METHOD_NOT_ALLOWED','Discovery requires GET.',{},405);
18
+ requireCondition(!url.search,'UNKNOWN_FIELDS','Discovery takes no parameters.');
19
+ requireCondition(req.headers.cookie===undefined&&req.headers.authorization===undefined,'CREDENTIALS_NOT_ACCEPTED','Discovery requires credentials: omit.',{},403);
20
+ }
21
+ if(req.method==='OPTIONS'){
22
+ const method=req.headers['access-control-request-method'],headers=req.headers['access-control-request-headers'],privateNetwork=req.headers['access-control-request-private-network'];
23
+ requireCondition(method==='GET'||(approved&&method===undefined),'METHOD_NOT_ALLOWED','Discovery preflight permits GET only.',{},405);
24
+ requireCondition(headers===undefined||headers.trim()==='','PREFLIGHT_HEADERS_DENIED','Discovery preflight accepts no additional request headers.',{},403);
25
+ requireCondition(privateNetwork===undefined||privateNetwork==='true','INVALID_PREFLIGHT','Invalid private-network preflight.');
26
+ cors=origin===undefined?{}:{'access-control-allow-origin':origin};
27
+ // A non-2xx preflight hides the subsequent refusal from fetch. This grants only the fixed GET response.
28
+ send(res,204,undefined,{...cors,vary:VARY,'access-control-allow-methods':'GET',...(origin!==undefined&&privateNetwork==='true'?{'access-control-allow-private-network':'true'}:{})});return;
29
+ }
30
+ cors=origin===undefined?{}:{'access-control-allow-origin':origin,vary:'Origin'};
31
+ if(!approved){send(res,403,{error:{code:'ORIGIN_DENIED',message:'This website is not allowed to read discovery.'}},cors);return;}
32
+ requireCondition(req.method==='GET','METHOD_NOT_ALLOWED','Discovery requires GET.',{},405);
33
+ requireCondition(!url.search,'UNKNOWN_FIELDS','Discovery takes no parameters.');
34
+ send(res,200,{...read(),traceId},cors);
35
+ }catch(error){
36
+ const trusted=error instanceof AppHostError;
37
+ send(res,trusted?error.status:500,{error:{code:trusted?error.code:'INTERNAL_ERROR',message:trusted?error.message:'The operation could not be completed.'},traceId},cors);
38
+ }
39
+ }