@whyshu/argent-engine 0.0.0-stage → 0.6.0

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 (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,314 @@
1
+ /**
2
+ * 离线设备绑定 —— **设备码(指纹摘要)+ 比对 + 自助重绑计数**(§3.6,2026-10-03)
3
+ *
4
+ * ## 一句话
5
+ * 新签发的许可载荷里可以带一份 `device` 声明(§3.6③);客户端只做**相等/不等**比对,
6
+ * 不解释它、不拿它当"已验签字段"(v1 的签名 payload 仍是 9 项,见下方"局限")。
7
+ *
8
+ * ## ⛔ 指纹只认一项本机事实:`IOPlatformUUID`(D3)
9
+ * 主机名 / MAC / IP / node 或 dsh 安装路径**一律禁止**当指纹 —— 它们会在客户什么都没干的时候变
10
+ * (换网络、改 WiFi、USB 网卡插拔、虚拟机克隆;node 一升级路径就变,而现网 dsh-web 的 macOS TCC
11
+ * 身份正好落在 `/opt/homebrew/bin/node` 上)。拿它们当指纹 = 客户"什么都没干就被判换机",
12
+ * **比不绑定更糟**(把授权变成随机故障)。⇒ 本文件是**唯一实现**:只 spawn 一条 `ioreg` 读它。
13
+ *
14
+ * ## 局限(如实标注,别让调用方误以为这是防伪)
15
+ * · v1 的签名字段固定 9 项(`core/lib/license-file.mjs`)⇒ **`device` 没有进签名**,
16
+ * 它是"**服务端声明 + 客户端比对**"。改了许可文件里的 `device` 不会让验签失败
17
+ * (签名覆盖的仍是那 9 项)—— 也就是说本机制**防君子不防小人**,它防的是"误装到别的机器",
18
+ * 不是"伪造授权"。要真防伪得服务端升 v2 把 `device` 纳入 payload(跨仓库,见 §6.2 A-⑨)。
19
+ * · 重绑计数落本机 `$ARGENT_DATA_DIR/license-rebind.json` ⇒ 它是**礼貌上限**,
20
+ * 不是可强制执行的限制:删掉那个文件 = 计数归零。我们不为此加密/混淆(那只会把
21
+ * "礼貌"伪装成"安全");真正的额度控制只能在服务端签发侧(每设备签几份)。
22
+ * · 非 Darwin 平台读不到该事实 ⇒ 按 **unbound 放行 + 明确标注**(不做 fail-closed:
23
+ * 陪跑式的宿主不保证是 macOS,见 §3.6 风险③)。
24
+ */
25
+ import { createHash } from 'node:crypto'
26
+ import { spawnSync } from 'node:child_process'
27
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
28
+ import { dirname, join } from 'node:path'
29
+ import { resolveDataDir } from './argent-datadir.mjs'
30
+
31
+ /** 本版只有一种指纹算法;将来换算法时靠这个标识**不破坏老文件**(§3.6③)。 */
32
+ export const DEVICE_ALGO = 'ioplatformuuid-sha256'
33
+ /** 自助重绑次数上限(D2:3 次);第 4 次起一律拒绝 + 人工指引。 */
34
+ export const MAX_REBINDS = 3
35
+ /** 设备码不匹配的宽限天数(D2:7 天)——宽限内 `state=grace`:提示但不拦。 */
36
+ export const REBIND_GRACE_DAYS = 7
37
+ export const REBIND_FILE = 'license-rebind.json'
38
+ const DAY_MS = 86400000
39
+
40
+ /**
41
+ * 读本机指纹**唯一来源**(`ioreg -rd1 -c IOPlatformExpertDevice` 的 IOPlatformUUID 行)。
42
+ * @param {{exec?:Function, platform?:string}} [opts] exec 供测试注入(夹具不跑真 ioreg)
43
+ * @returns {{ok:boolean, uuid:string, reason:string, error:string}}
44
+ * reason:`ok` | `device_unsupported`(非 Darwin ⇒ 本机不支持设备绑定)| `device_read_failed`
45
+ */
46
+ export function readFingerprint({ exec = spawnSync, platform = process.platform } = {}) {
47
+ if (platform !== 'darwin') {
48
+ return { ok: false, uuid: '', reason: 'device_unsupported',
49
+ error: `本机不支持设备绑定(platform=${platform}):设备码只认 IOPlatformUUID(仅 macOS 提供)⇒ 按 unbound 放行(不 fail-closed)` }
50
+ }
51
+ const r = exec('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { encoding: 'utf8', timeout: 10000 })
52
+ if (r && r.error) return { ok: false, uuid: '', reason: 'device_read_failed', error: `读本机指纹失败:${r.error.message || r.error}` }
53
+ const out = String((r && r.stdout) || '')
54
+ if ((r && r.status) !== 0 && !out) {
55
+ return { ok: false, uuid: '', reason: 'device_read_failed',
56
+ error: `ioreg 退出码 ${(r && r.status) ?? '?'}:${String((r && r.stderr) || '').trim().slice(0, 200) || '无输出'}` }
57
+ }
58
+ const m = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)
59
+ if (!m || !m[1].trim()) {
60
+ return { ok: false, uuid: '', reason: 'device_read_failed', error: 'ioreg 输出里没有 IOPlatformUUID(虚拟机/受限环境?)' }
61
+ }
62
+ return { ok: true, uuid: m[1].trim(), reason: 'ok', error: '' }
63
+ }
64
+
65
+ /**
66
+ * 设备码 = `sha256("<algo>:<指纹>")` 的 hex(小写,64 位)。
67
+ * 服务端与客户端**都必须用这一条**(算法标识进摘要 ⇒ 换算法不会撞码)。
68
+ */
69
+ export function deviceCodeOf(uuid, algo = DEVICE_ALGO) {
70
+ return createHash('sha256').update(`${algo}:${String(uuid)}`).digest('hex')
71
+ }
72
+
73
+ /**
74
+ * 本机设备码(§3.6① 第 1 步 `license request` 就是打印它)。
75
+ * @returns {{ok:boolean, code:string, algo:string, issuedAt:string, reason:string, error:string, uuid:string}}
76
+ * ⚠️ `uuid` 是**内部字段**(只给比对/测试用),对外输出(`--json`)**不要**打印原始 UUID。
77
+ */
78
+ export function currentDevice({ exec = spawnSync, platform = process.platform, now = new Date() } = {}) {
79
+ const fp = readFingerprint({ exec, platform })
80
+ const issuedAt = now instanceof Date ? now.toISOString() : new Date(now).toISOString()
81
+ if (!fp.ok) return { ok: false, code: '', algo: DEVICE_ALGO, issuedAt, reason: fp.reason, error: fp.error, uuid: '' }
82
+ return { ok: true, code: deviceCodeOf(fp.uuid), algo: DEVICE_ALGO, issuedAt, reason: 'ok', error: '', uuid: fp.uuid }
83
+ }
84
+
85
+ /**
86
+ * 许可载荷里的 `device` 声明(v1 里**未进签名**;只校验形状,不做任何解释)。
87
+ * ⚠️ v2 的载荷归一要用**更严**的那一份:见文件末尾 `deviceClaimForSignature()`
88
+ * (要求 `issuedAt` 是可解析时间;形状坏 ⇒ 拒绝,而不是"当没声明")。
89
+ * @returns {{code:string, algo:string, issuedAt:string}|null} 缺失/形状非法 ⇒ null(= unbound 放行)
90
+ */
91
+ export function deviceClaimOf(file) {
92
+ const d = file && typeof file === 'object' && !Array.isArray(file) ? file.device : null
93
+ if (!d || typeof d !== 'object' || Array.isArray(d)) return null
94
+ const code = String(d.code || '').trim()
95
+ if (!code) return null
96
+ return { code, algo: String(d.algo || DEVICE_ALGO).trim(), issuedAt: String(d.issuedAt || '') }
97
+ }
98
+
99
+ // ── 自助重绑计数(落 $ARGENT_DATA_DIR;不联网、不入注册表)────────────────────
100
+ export function rebindStorePath(env = process.env) {
101
+ return join(resolveDataDir({ env }).dir, REBIND_FILE)
102
+ }
103
+
104
+ /** 空计数(缺文件/坏文件统一形状:调用方不用判字段存在性)。 */
105
+ function emptyStore() {
106
+ return { v: 1, count: 0, acceptedCodes: [], rebinds: [], firstMismatchAt: null, broken: false }
107
+ }
108
+
109
+ /**
110
+ * 读重绑计数。缺文件 ⇒ 全 0(**首次**运行是正常态,不是错误);坏文件 ⇒ 全 0 + `broken:true`
111
+ * (宁可让客户重绑,也不因一个坏计数把人锁死)。
112
+ */
113
+ export function readRebindStore({ path, env = process.env } = {}) {
114
+ const p = path || rebindStorePath(env)
115
+ if (!existsSync(p)) return { ...emptyStore(), path: p }
116
+ try {
117
+ const j = JSON.parse(readFileSync(p, 'utf8'))
118
+ const codes = Array.isArray(j.acceptedCodes) ? j.acceptedCodes.map(String).filter(Boolean) : []
119
+ return {
120
+ v: Number(j.v || 1),
121
+ count: Number.isFinite(Number(j.count)) ? Number(j.count) : 0,
122
+ acceptedCodes: codes,
123
+ rebinds: Array.isArray(j.rebinds) ? j.rebinds : [],
124
+ firstMismatchAt: j.firstMismatchAt ? String(j.firstMismatchAt) : null,
125
+ broken: false,
126
+ path: p,
127
+ }
128
+ } catch {
129
+ return { ...emptyStore(), broken: true, path: p }
130
+ }
131
+ }
132
+
133
+ /** 原子写计数(0600;失败不抛 —— 计数写不下**不该**让业务/安装失败)。 */
134
+ export function writeRebindStore(store, { path, env = process.env } = {}) {
135
+ const p = path || rebindStorePath(env)
136
+ try {
137
+ mkdirSync(dirname(p), { recursive: true, mode: 0o700 })
138
+ const body = {
139
+ v: 1,
140
+ count: Number(store.count || 0),
141
+ acceptedCodes: [...new Set((store.acceptedCodes || []).map(String).filter(Boolean))],
142
+ rebinds: store.rebinds || [],
143
+ firstMismatchAt: store.firstMismatchAt || null,
144
+ }
145
+ const tmp = `${p}.tmp`
146
+ writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`, { mode: 0o600 })
147
+ chmodSync(tmp, 0o600)
148
+ renameSync(tmp, p)
149
+ return { ok: true, path: p }
150
+ } catch (e) {
151
+ return { ok: false, path: p, error: String(e.message || e) }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 设备绑定判定(**纯函数**,唯一判据;调用方不需要也不得另写一套)。
157
+ *
158
+ * @param {object} o
159
+ * @param {object|null} o.claimed 许可载荷里的 `device`(`deviceClaimOf` 的结果;null = unbound)
160
+ * @param {object} o.current `currentDevice()` 的结果
161
+ * @param {object} o.store `readRebindStore()` 的结果
162
+ * @param {number} [o.now]
163
+ * @returns {{binding:'unbound'|'match'|'rebound'|'mismatch'|'unsupported', reason:string,
164
+ * sanctioned:boolean, state:'valid'|'grace'|'denied', graceUntil:string|null,
165
+ * graceLeftDays:number|null, rebindsLeft:number, machineCode:string, claimedCode:string,
166
+ * note:string, warnings:string[]}}
167
+ * binding:unbound(无声明 ⇒ **放行**,存量许可不受影响)|match |rebound(自助重绑过 ⇒ 放行)
168
+ * |mismatch(不匹配:宽限内 grace / 超限或次数用尽 denied)|unsupported(读不到指纹 ⇒ 放行+标注)
169
+ */
170
+ export function bindingOf({ claimed = null, current, store, now = Date.now() } = {}) {
171
+ const cur = current || { ok: false, code: '', reason: 'device_read_failed', error: '未读本机指纹' }
172
+ const st = store || emptyStore()
173
+ const rebindsLeft = Math.max(0, MAX_REBINDS - Number(st.count || 0))
174
+ const machineCode = cur.code || ''
175
+ const base = { graceUntil: null, graceLeftDays: null, rebindsLeft, machineCode, claimedCode: claimed ? claimed.code : '', warnings: [] }
176
+
177
+ // ① 无 device 声明 ⇒ unbound 放行(D1-兼容:**不得**让存量许可失效)
178
+ if (!claimed) {
179
+ return { ...base, binding: 'unbound', reason: 'unbound', sanctioned: true, state: 'valid',
180
+ note: '许可未声明设备(unbound)⇒ 不做比对、按今天的三态判(存量许可继续有效)' }
181
+ }
182
+ // ② 读不到本机指纹 / 算法不认识 ⇒ 无法比对 ⇒ 放行 + **明确标注**(不做 fail-closed)
183
+ if (!cur.ok) {
184
+ return { ...base, binding: 'unsupported', reason: cur.reason || 'device_read_failed', sanctioned: true, state: 'valid',
185
+ note: `${cur.error || '读不到本机指纹'} ⇒ 无法比对,按 unbound 放行并如实标注`,
186
+ warnings: ['本机读不到设备指纹:本次**没有**做设备绑定校验(不是"校验通过")'] }
187
+ }
188
+ if (claimed.algo && claimed.algo !== DEVICE_ALGO) {
189
+ return { ...base, binding: 'unsupported', reason: 'device_algo_unknown', sanctioned: true, state: 'valid',
190
+ note: `许可声明的指纹算法 ${claimed.algo} 本客户端不认识(只认 ${DEVICE_ALGO})⇒ 无法比对,按 unbound 放行`,
191
+ warnings: [`设备指纹算法不认识(${claimed.algo})⇒ 本次没有做设备绑定校验`] }
192
+ }
193
+ // ③ 相等 / 之前自助重绑过 ⇒ 放行
194
+ if (claimed.code === machineCode) {
195
+ return { ...base, binding: 'match', reason: 'device_match', sanctioned: true, state: 'valid',
196
+ note: `设备码一致(${machineCode.slice(0, 12)}…)` }
197
+ }
198
+ if ((st.acceptedCodes || []).includes(claimed.code)) {
199
+ return { ...base, binding: 'rebound', reason: 'device_rebound', sanctioned: true, state: 'valid',
200
+ note: `本机自助重绑过这份许可的设备码(已用 ${Number(st.count || 0)}/${MAX_REBINDS} 次)⇒ 放行` }
201
+ }
202
+
203
+ // ④ 不匹配:次数用尽 ⇒ 拒绝;宽限内 ⇒ grace(提示但不拦);超宽限 ⇒ 拒绝
204
+ if (rebindsLeft <= 0) {
205
+ return { ...base, binding: 'mismatch', reason: 'rebind_exhausted', sanctioned: false, state: 'denied',
206
+ claimedCode: claimed.code,
207
+ note: `设备码不匹配,且自助重绑 ${MAX_REBINDS} 次已用尽 ⇒ 需人工:让服务端重签一份绑定本机设备码的许可`
208
+ + `(本机设备码见 \`argent license request\`),或人工重绑(见 §3.6② D2)`,
209
+ warnings: ['设备码不匹配且自助重绑次数已用尽(rebind_exhausted)⇒ 拒绝'] }
210
+ }
211
+ const started = st.firstMismatchAt ? Date.parse(st.firstMismatchAt) : NaN
212
+ const graceUntilMs = (Number.isFinite(started) ? started : now) + REBIND_GRACE_DAYS * DAY_MS
213
+ const graceUntil = new Date(graceUntilMs).toISOString()
214
+ if (now > graceUntilMs) {
215
+ return { ...base, binding: 'mismatch', reason: 'device_mismatch', sanctioned: false, state: 'denied', graceUntil, graceLeftDays: 0,
216
+ note: `设备码不匹配且 ${REBIND_GRACE_DAYS} 天宽限已过(至 ${graceUntil})⇒ 拒绝;自助重绑还剩 ${rebindsLeft} 次`
217
+ + '(`argent license install --file <许可> --rebind --apply`)',
218
+ warnings: [`设备码不匹配且宽限期已过(device_mismatch)⇒ 拒绝`] }
219
+ }
220
+ return { ...base, binding: 'mismatch', reason: 'device_mismatch', sanctioned: true, state: 'grace', graceUntil,
221
+ graceLeftDays: Math.max(0, (graceUntilMs - now) / DAY_MS),
222
+ note: `设备码不匹配(本机 ${machineCode.slice(0, 12)}… ≠ 许可 ${claimed.code.slice(0, 12)}…)`
223
+ + ` ⇒ 宽限期内放行(剩 ${Math.max(0, (graceUntilMs - now) / DAY_MS).toFixed(1)} 天,至 ${graceUntil})`
224
+ + `,请尽快自助重绑(还剩 ${rebindsLeft} 次)或让服务端重签`,
225
+ warnings: [`设备码与许可声明的设备不一致(宽限期内,剩 ${Math.max(0, (graceUntilMs - now) / DAY_MS).toFixed(1)} 天)—— 业务放行,但必须上报`] }
226
+ }
227
+
228
+ /**
229
+ * 记一次"自助重绑"(**写**:只在真写路径调用,dry-run 绝不调)。
230
+ * 计数 +1、把被接受的设备码记进 `acceptedCodes`(此后 `bindingOf` 判 `rebound`)。
231
+ */
232
+ export function recordRebind(store, { code, at = new Date().toISOString(), path, env = process.env } = {}) {
233
+ const next = {
234
+ v: 1,
235
+ count: Number(store?.count || 0) + 1,
236
+ acceptedCodes: [...(store?.acceptedCodes || []), String(code || '')].filter(Boolean),
237
+ rebinds: [...(store?.rebinds || []), { at, code: String(code || '') }],
238
+ firstMismatchAt: store?.firstMismatchAt || at,
239
+ }
240
+ const w = writeRebindStore(next, { path, env })
241
+ return { ...next, ok: w.ok, path: w.path, error: w.error }
242
+ }
243
+
244
+ /** 记"第一次发现不匹配"的时刻(宽限 7 天的起点;已记过就不动 —— 幂等)。 */
245
+ export function noteMismatch(store, { at = new Date().toISOString(), path, env = process.env } = {}) {
246
+ if (store?.firstMismatchAt) return { ...store, ok: true, path: store.path }
247
+ const next = {
248
+ v: 1,
249
+ count: Number(store?.count || 0),
250
+ acceptedCodes: store?.acceptedCodes || [],
251
+ rebinds: store?.rebinds || [],
252
+ firstMismatchAt: at,
253
+ }
254
+ const w = writeRebindStore(next, { path, env })
255
+ return { ...next, ok: w.ok, path: w.path, error: w.error }
256
+ }
257
+
258
+ /** 只读展示用:计数文件是否可读、权限是否 0600(不拦人,只如实报)。 */
259
+ export function rebindStoreInfo({ path, env = process.env } = {}) {
260
+ const p = path || rebindStorePath(env)
261
+ let mode = null
262
+ try { if (existsSync(p)) mode = `0${(statSync(p).mode & 0o777).toString(8)}` } catch { /* 拿不到不致命 */ }
263
+ return { path: p, mode }
264
+ }
265
+
266
+ // ── v2 签名载荷里的 device 形状(**严格版**,2026-10-03)──────────────────────
267
+ // 与上面的 `deviceClaimOf` 的**唯一区别**(v1 判据一字不改,故不合并):
268
+ // · `deviceClaimOf` 面向 v1(**不参与签名**)⇒ 宽容:缺字段补默认、形状坏 ⇒ 当没声明(放行);
269
+ // · 本函数面向 v2(**要进签名**)⇒ 严格:三键必须写全且合法,形状坏 ⇒ **拒绝**(malformed),
270
+ // 绝不"当没声明"(否则"绑定写坏了"会被静默降级成"没绑定"= 一次静默的授权放宽)。
271
+ // 缺位(键不存在 **或** 值为 null)= **未声明**,是**合法**状态;它同样进签名("缺位"被覆盖)。
272
+
273
+ /** 服务端/客户端共用的时间形态:ISO(UTC、毫秒精度)。不可解析 ⇒ null。 */
274
+ function toIsoStrict(v) {
275
+ if (v === null || v === undefined || v === '') return null
276
+ const t = Date.parse(String(v))
277
+ return Number.isFinite(t) ? new Date(t).toISOString() : null
278
+ }
279
+
280
+ /**
281
+ * 归一无符号的 v2 `device` 声明(**签名载荷专用**)。
282
+ * @returns {{ok:boolean, present:boolean, device:{code:string,algo:string,issuedAt:string}|null, warning:string}}
283
+ * ok:false ⇒ 形状非法(调用方按 `malformed` 拒绝,**不得**退回 unbound)
284
+ * present:false ⇒ 未声明(缺位或 null)⇒ payload 里**不含** device 键
285
+ */
286
+ export function deviceClaimForSignature(file) {
287
+ const f = file && typeof file === 'object' && !Array.isArray(file) ? file : null
288
+ const raw = f ? f.device : null
289
+ if (raw === null || raw === undefined) return { ok: true, present: false, device: null, warning: '' }
290
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
291
+ return { ok: false, present: true, device: null,
292
+ warning: 'v2 许可的 `device` 必须是对象(三键:code/algo/issuedAt)⇒ 判 malformed(不降级成 unbound)' }
293
+ }
294
+ // 三键**必须写全**(缺任一 ⇒ malformed):签名载荷按"恰好这三键"构造,
295
+ // 让"缺 algo ⇒ 客户端补默认"这类隐形归一消失(那会让两侧 payload 悄悄漂移)。
296
+ const missing = ['code', 'algo', 'issuedAt'].filter((k) => raw[k] === undefined || raw[k] === null || raw[k] === '')
297
+ if (missing.length) {
298
+ return { ok: false, present: true, device: null,
299
+ warning: `v2 许可的 \`device\` 缺字段 ${missing.join('/')}(v2 要求三键写全:code/algo/issuedAt)⇒ 判 malformed` }
300
+ }
301
+ if (typeof raw.code !== 'string' || !raw.code.trim()) {
302
+ return { ok: false, present: true, device: null, warning: 'v2 许可的 `device.code` 必须是非空字符串 ⇒ 判 malformed' }
303
+ }
304
+ if (typeof raw.algo !== 'string' || !raw.algo.trim()) {
305
+ return { ok: false, present: true, device: null, warning: 'v2 许可的 `device.algo` 必须是非空字符串 ⇒ 判 malformed' }
306
+ }
307
+ const issuedAt = toIsoStrict(raw.issuedAt)
308
+ if (!issuedAt) {
309
+ return { ok: false, present: true, device: null,
310
+ warning: 'v2 许可的 `device.issuedAt` 不是可解析时间(必须是与 issuedAt/expiresAt 同规的 ISO 形态)⇒ 判 malformed' }
311
+ }
312
+ // 只取这三键进签名:device 里的**额外键不参与签名**,客户端也**不得**读它们(多键不等于多判据)
313
+ return { ok: true, present: true, device: { code: raw.code.trim(), algo: raw.algo.trim(), issuedAt }, warning: '' }
314
+ }