@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,577 @@
1
+ /**
2
+ * `argent license <sub>` 适配层(P1 第二批,2026-10-03)—— 把**既有判据**接成机器可判的 verb。
3
+ *
4
+ * ## 纪律(§3.5①):本文件**不复制任何判据**
5
+ * | 子命令 | 判据在哪(唯一落点) | 本层只做 |
6
+ * |---|---|---|
7
+ * | `status` | `scripts/check-license.mjs --json`(← `core/lib/license-verdict.mjs` 三态 + 设备) | 透传退出码 0/3/1/2 + A2 回显 |
8
+ * | `verify` | `core/lib/license-verdict.mjs`(验签 + 判期 + 设备) | 把"验签本身"单独答出来(**不落盘**) |
9
+ * | `install` | `scripts/install-license.mjs`(验签后落盘 0600 + 备份 + 设备比对 + 自助重绑 + **现役比对**) | 默认 dry-run、`--apply` 才真写 |
10
+ * | `request` | `core/lib/license-device.mjs`(指纹**只认硬件 UUID 一项**,D3) | 打印设备码(离线激活第一步) |
11
+ * | `renew` | 离线 = "验签一份新文件"(走 install 同一条路);在线端点属外部依赖(策划案②) | 没有新文件 ⇒ **成功但空**(`empty:true`),**绝不**当 denied |
12
+ * | `explain` | 同上(只重组 `check-license --json` 的既有字段) | 把三态翻译成"缺什么 / 下一步做什么" |
13
+ *
14
+ * ## A1/A2/A3
15
+ * · A1:`0` 成功(含"成功但空")|`3` **仅**许可宽限放行|`2` 用法/环境(缺 `--file`、显式路径不存在、
16
+ * 指纹读不到)|`1` 运行失败(验签不过 / 超宽限 / **设备不匹配且自助重绑用尽** ⇒ 被拒)。
17
+ * · A2:`ok/empty/code/reason` 恒在;`$? == json.code`;`--json` 时 stdout 只有一份 JSON。
18
+ * 回显 `licenseFile`/`state`/`expiresAt`/`graceDays` + **签名版本**(`signatureVersion` 1|2|null)
19
+ * + 设备绑定态(binding/reason/rebindsLeft + `signed`/`claimSigned`)。
20
+ * `signed` 的语义**按版本**:v1 ⇒ `false`(`device` 未进签名,服务端声明 + 客户端比对);
21
+ * v2 ∧ 验签通过 ⇒ `true`(声明的**内容或它的缺位**都被签名覆盖)—— 判据来自验签结果,本层不重算。
22
+ * · A3:`install`/`renew` 是写类 ⇒ **默认 dry-run**(真写要 `--apply`/`--yes`/`--confirm`);
23
+ * dry-run **零持久副作用**(不落许可、不备份、不写重绑计数)—— 判据 = 跑两次后数据目录逐字节不变。
24
+ * · A4(装机/续期侧,2026-10-04):**幂等** = 与现役**载荷+签名**逐字节一致 ⇒ `unchanged:true`(不备份、不重写);
25
+ * **拒绝倒退** = 比现役更早到期(`stale`)或能力/席位被收窄(`narrowed`)⇒ `regression_refused`(exit 1,不落盘),
26
+ * 显式 `--allow-regression` 才放行;**换绑**(`renewal.deviceChanged`)只如实标注 —— 设备侧的拒绝归 ② 的比对。
27
+ *
28
+ * ## `renew` 与文档口径的一处如实差异
29
+ * 实施方案 §3.5① 建议 `renew` "包 `scripts/lease.py`(--status/--check)"。本层**刻意不包**:
30
+ * 租约(能力订阅)与离线许可是**两条授权链路**,`lease.py` 的判据归 `flow-trigger.sh` 的租约闸门;
31
+ * 把它的结论塞进 `renew` 只会让"我到底缺哪一样"更难回答(§2.2④ 那种恒 `ok:true` 的翻版)。
32
+ * `renew` 只回答一件事:**有没有一份可用的新许可文件**;没有 ⇒ `empty:true + no_refresh_source`。
33
+ */
34
+ import { spawnSync } from 'node:child_process'
35
+ import { existsSync, readFileSync } from 'node:fs'
36
+ import { join, resolve } from 'node:path'
37
+ import { licenseVerdict } from './license-verdict.mjs'
38
+ import { MAX_REBINDS, REBIND_GRACE_DAYS, currentDevice, rebindStoreInfo, readRebindStore } from './license-device.mjs'
39
+ import { ACTIVATE_BOOL_FLAGS, ACTIVATE_VALUE_FLAGS, ACTIVATE_USAGE, activateVerb } from './license-activate.mjs'
40
+ // 只读运营巡检(O2:替代 `mongosh`)—— 参数表**从实现模块导入**(不在这里再写一份,避免"帮助里有、解析里没有")
41
+ import { OPS_BOOL_FLAGS, OPS_REPEAT_FLAGS, OPS_VALUE_FLAGS, runOpsVerb } from './license-ops.mjs'
42
+
43
+ /** 本组 verb 的子命令(与 `core/contracts/argent.contracts.json#verbs.license.subcommands` 逐字对齐)。 */
44
+ export const LICENSE_SUBCOMMANDS = ['status', 'verify', 'install', 'request', 'renew', 'explain', 'activate', 'ops']
45
+ const CONFIRM_FLAGS = ['--apply', '--yes', '--confirm']
46
+
47
+ /** 参数规格:`value` 带值(可重复的放 `repeat`),`bool` 无值。 */
48
+ const SPEC = {
49
+ status: { value: ['--file', '--vertical', '--now'], repeat: ['--cap'], bool: [] },
50
+ verify: { value: ['--file', '--secret-file', '--now'], repeat: [], bool: [] },
51
+ install: { value: ['--file', '--secret-file', '--now', '--sign-version', '--device-code'], repeat: [], bool: ['--rebind', '--sign-v2', '--device-from-local', '--no-device', '--allow-regression', ...CONFIRM_FLAGS, '--dry-run'] },
52
+ request: { value: [], repeat: [], bool: [] },
53
+ renew: { value: ['--file', '--secret-file', '--now', '--sign-version', '--device-code'], repeat: [], bool: ['--sign-v2', '--device-from-local', '--no-device', '--rebind', '--allow-regression', ...CONFIRM_FLAGS, '--dry-run', '--online'] },
54
+ explain: { value: ['--file', '--vertical', '--now'], repeat: ['--cap'], bool: [] },
55
+ // 一次性激活码兑换(§9):**默认 dry-run**(零网络 + 零落盘),`--apply` 才真兑换;
56
+ // 参数表与 `license-activate.mjs#ACTIVATE_*_FLAGS` 同源(不各写一份,避免"帮助里有、解析里没有")。
57
+ activate: { value: [...ACTIVATE_VALUE_FLAGS], repeat: [], bool: [...ACTIVATE_BOOL_FLAGS] },
58
+ // 只读运营巡检(O2):**读类**,没有 `--apply`/`--yes` —— 参数表与 `license-ops.mjs#OPS_*_FLAGS` 同源。
59
+ // ⚠️ 它的 argv 解析在 `runOpsVerb` 内部(同一张表),因此这里不做 `parseSubArgs('ops', …)`(避免两份解析)。
60
+ ops: { value: [...OPS_VALUE_FLAGS], repeat: [...OPS_REPEAT_FLAGS], bool: [...OPS_BOOL_FLAGS] },
61
+ }
62
+ const KNOWN = new Set([...Object.values(SPEC).flatMap((s) => [...s.value, ...s.repeat, ...s.bool]), '--json'])
63
+
64
+ /** 解析 argv(**未知参数一律拒绝** ⇒ 2;"别的参数我替你吃了"是错误推迟到脚本里才暴露的经典坑)。 */
65
+ export function parseSubArgs(sub, argv = []) {
66
+ const spec = SPEC[sub] || { value: [], repeat: [], bool: [] }
67
+ const values = {}
68
+ const repeats = {}
69
+ const bools = new Set()
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const a = argv[i]
72
+ if (a === '--json') continue
73
+ if (!KNOWN.has(a)) return { ok: false, reason: 'usage_error', error: `未知参数:${a}(用法:argent license ${sub} …)` }
74
+ if (spec.bool.includes(a)) { bools.add(a); continue }
75
+ if (spec.value.includes(a) || spec.repeat.includes(a)) {
76
+ const v = argv[i + 1]
77
+ if (v === undefined || String(v).startsWith('--')) return { ok: false, reason: 'usage_error', error: `参数 ${a} 缺值` }
78
+ if (spec.repeat.includes(a)) (repeats[a] ||= []).push(v)
79
+ else values[a] = v
80
+ i++
81
+ continue
82
+ }
83
+ // 属于别的子命令的合法参数 ⇒ 明确报"这个子命令没有它"(不要静默忽略)
84
+ return { ok: false, reason: 'usage_error', error: `子命令 ${sub} 不支持参数:${a}` }
85
+ }
86
+ return { ok: true, values, repeats, bools }
87
+ }
88
+
89
+ const flag = (v) => (v === undefined ? null : v)
90
+ const envOf = (deps) => deps.env || process.env
91
+ /** 子进程(可注入便于夹具)。 */
92
+ function runNode(script, args, deps = {}) {
93
+ const spawn = deps.spawn || spawnSync
94
+ const r = spawn(process.execPath, [script, ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, env: envOf(deps) })
95
+ return { status: r.status === null || r.status === undefined ? 1 : Number(r.status), stdout: r.stdout || '', stderr: r.stderr || '', error: r.error || null }
96
+ }
97
+ /** 解析子进程 stdout 的 JSON(失败 ⇒ null,由调用方转成 bad_output)。 */
98
+ function parseJson(text) {
99
+ const s = String(text || '')
100
+ const i = s.indexOf('{')
101
+ if (i < 0) return null
102
+ try { return JSON.parse(s.slice(i)) } catch { return null }
103
+ }
104
+ /** 统一失败信封(A2)。 */
105
+ const failJson = (code, reason, error, extra = {}) => ({
106
+ json: { ok: false, empty: false, code, reason, error, ...extra },
107
+ human: `❌ ${reason}:${error}`,
108
+ stderr: '',
109
+ exit: code,
110
+ })
111
+ /** 设备回显(对外形状;`signed` = 该版本的签名载荷**是否覆盖 device 这一项**,见 §3.6③)。 */
112
+ function deviceEcho(d) {
113
+ if (!d) return null
114
+ return {
115
+ present: d.present, signed: d.signed === true, claimSigned: d.claimSigned === true,
116
+ binding: d.binding, reason: d.reason, sanctioned: d.sanctioned,
117
+ code: d.code, algo: d.algo, issuedAt: d.issuedAt, machineCode: d.machineCode,
118
+ machineReadable: d.machineReadable, rebindsLeft: d.rebindsLeft,
119
+ graceUntil: d.graceUntil, graceLeftDays: d.graceLeftDays, note: d.note,
120
+ }
121
+ }
122
+ /** A2 的 `reason` 只认词表里的短码(`argent.contracts.json#reasonCodes`);这里做**边界翻译**。 */
123
+ function reasonOf({ allowed, state, gateReason, licenseReason }) {
124
+ if (allowed) return state === 'grace' ? 'license_grace' : null
125
+ return gateReason || licenseReason || 'guard_rejected'
126
+ }
127
+
128
+ // ── status ─────────────────────────────────────────────────────────────────
129
+ function statusVerb(sub, args, deps) {
130
+ const p = parseSubArgs(sub, args)
131
+ if (!p.ok) return failJson(2, p.reason, p.error)
132
+ const repo = deps.repo
133
+ const cli = [join(repo, 'scripts', 'check-license.mjs'), '--json']
134
+ if (p.values['--file']) cli.push('--file', p.values['--file'])
135
+ if (p.values['--vertical']) cli.push('--vertical', p.values['--vertical'])
136
+ if (p.values['--now']) cli.push('--now', p.values['--now'])
137
+ for (const c of p.repeats['--cap'] || []) cli.push('--cap', c)
138
+ const child = runNode(cli[0], cli.slice(1), deps)
139
+ const j = parseJson(child.stdout)
140
+ if (!j) {
141
+ return failJson(1, 'bad_output',
142
+ `check-license 输出不可解析(exit=${child.status}):${String(child.stderr || child.stdout).trim().slice(0, 300)}`)
143
+ }
144
+ const allowed = j.allowed === true
145
+ const reason = reasonOf({ allowed, state: j.state, gateReason: j.gateReason, licenseReason: j.reason })
146
+ const json = {
147
+ ok: allowed,
148
+ empty: false,
149
+ code: allowed ? (j.state === 'grace' ? 3 : 0) : 1,
150
+ reason,
151
+ // 回显(A2 纪律:生效值 + 判据来源,一眼能看出"真没有"还是"被别人拒了")
152
+ subcommand: 'status',
153
+ licenseFile: j.path,
154
+ licenseMode: j.mode,
155
+ state: j.state,
156
+ licenseReason: j.reason, // 原始原因(missing/bad_signature/expired/…),供诊断
157
+ expiresAt: j.expiresAt || null,
158
+ issuedAt: j.issuedAt || null,
159
+ graceDays: j.graceDays,
160
+ graceUntil: j.graceUntil || null,
161
+ inGrace: j.inGrace === true,
162
+ graceSource: j.graceSource || null,
163
+ // 签名版本(1|2|null)+ `device` 是否进签名(v1 ⇒ false;v2 ∧ 验签通过 ⇒ true)
164
+ signatureVersion: j.signatureVersion ?? null,
165
+ deviceSigned: j.device && j.device.signed === true,
166
+ userId: j.userId || '',
167
+ seats: j.seats,
168
+ capabilities: j.capabilities || [],
169
+ verticals: j.verticals || [],
170
+ requestedCaps: j.requestedCaps || [],
171
+ requestedVertical: j.requestedVertical || null,
172
+ gateReason: j.gateReason || null,
173
+ device: j.device || null,
174
+ conclusion: j.conclusion || '',
175
+ warnings: j.warnings || [],
176
+ // 证据/口径来源(**判定点唯一在 CLI 进程内**:本 verb 只是透传,不自己算)
177
+ judgedBy: 'scripts/check-license.mjs',
178
+ }
179
+ const human = humanStatus(json)
180
+ return { json, human, stderr: '', exit: Number(json.code) }
181
+ }
182
+
183
+ function humanStatus(j) {
184
+ const mark = j.ok ? (j.state === 'grace' ? '⚠️' : '✅') : '❌'
185
+ const lines = [`${mark} 许可状态:${j.state}(${j.reason || j.licenseReason || 'ok'})`]
186
+ lines.push(` 文件:${j.licenseFile}${j.licenseMode ? `(${j.licenseMode})` : ''}`)
187
+ lines.push(` 签名版本:${j.signatureVersion === 2 ? 'v2(device 已进签名)' : (j.signatureVersion === 1 ? 'v1(device 未进签名)' : '判不出')}`)
188
+ if (j.expiresAt) lines.push(` 账号:${j.userId || '(未写)'} | 席位:${j.seats} | 到期:${j.expiresAt}(宽限 ${j.graceDays} 天至 ${j.graceUntil})`)
189
+ if (j.capabilities.length || j.verticals.length) lines.push(` 能力:${j.capabilities.join(',') || '(空 ⇒ 拒一切能力)'} | 业务线:${j.verticals.join(',') || '(空 = 不限)'}`)
190
+ if (j.device) {
191
+ lines.push(` 设备:${j.device.binding}(${j.device.reason})| 重绑还剩 ${j.device.rebindsLeft} 次`
192
+ + `${j.device.machineCode ? ` | 本机设备码 ${String(j.device.machineCode).slice(0, 12)}…` : ''}`)
193
+ lines.push(` ${deviceSignedText({ signatureVersion: j.signatureVersion, device: j.device })}`)
194
+ }
195
+ if (j.conclusion) lines.push(` 结论:${j.conclusion}`)
196
+ for (const w of j.warnings) lines.push(` ⚠️ ${w}`)
197
+ return lines.join('\n')
198
+ }
199
+
200
+ // ── verify ─────────────────────────────────────────────────────────────────
201
+ function verifyVerb(sub, args, deps) {
202
+ const p = parseSubArgs(sub, args)
203
+ if (!p.ok) return failJson(2, p.reason, p.error)
204
+ const explicit = flag(p.values['--file'])
205
+ if (explicit && !existsSync(resolve(explicit))) {
206
+ // 显式给了路径却不存在 ⇒ **用法/环境错误(2)**,不是"许可被拒"(这两件事的处置完全不同)
207
+ return failJson(2, 'usage_error', `--file 指向的许可文件不存在:${resolve(explicit)}`)
208
+ }
209
+ const now = p.values['--now'] ? Date.parse(p.values['--now']) : Date.now()
210
+ if (Number.isNaN(now)) return failJson(2, 'usage_error', `--now 不是可解析的时间:${p.values['--now']}`)
211
+ const v = licenseVerdict({ file: explicit ? resolve(explicit) : undefined, now, secretPath: p.values['--secret-file'] ? resolve(p.values['--secret-file']) : undefined })
212
+ const signatureOk = v.baseOk === true
213
+ const json = {
214
+ ok: v.ok === true,
215
+ empty: false,
216
+ code: v.exit,
217
+ reason: v.ok === true ? (v.state === 'grace' ? 'license_grace' : null) : (v.device && v.device.binding === 'mismatch' ? v.device.reason : v.reason),
218
+ subcommand: 'verify',
219
+ licenseFile: v.path,
220
+ state: v.state,
221
+ signatureVersion: v.signatureVersion === 2 ? 2 : (v.signatureVersion === 1 ? 1 : null),
222
+ deviceSigned: v.deviceSigned === true,
223
+ signature: { ok: signatureOk, reason: v.reason, version: v.signatureVersion === 2 ? 2 : (v.signatureVersion === 1 ? 1 : null),
224
+ algorithm: 'HMAC-SHA256', deviceSigned: v.deviceSigned === true },
225
+ device: deviceEcho(v.device),
226
+ userId: v.userId || '',
227
+ seats: v.seats,
228
+ expiresAt: v.expiresAt || null,
229
+ issuedAt: v.issuedAt || null,
230
+ graceDays: v.graceDays,
231
+ graceUntil: v.graceUntil || null,
232
+ graceSource: v.graceSource || null,
233
+ inGrace: v.inGrace === true,
234
+ capabilities: v.capabilities || [],
235
+ verticals: v.verticals || [],
236
+ warnings: v.warnings || [],
237
+ judgedBy: 'core/lib/license-verdict.mjs',
238
+ }
239
+ const human = (signatureOk
240
+ ? `${v.ok ? (v.state === 'grace' ? '⚠️' : '✅') : '❌'} 验签通过(${v.reason})⇒ state=${v.state}`
241
+ : `❌ 验签未通过(${v.reason})`)
242
+ + `\n 文件:${v.path}`
243
+ + `\n 签名版本:${json.signatureVersion === null ? '判不出' : `v${json.signatureVersion}`}`
244
+ + ` | device 进签名:${v.deviceSigned === true ? '是' : '否'}`
245
+ + (json.device ? `\n 设备:${json.device.binding}(${json.device.reason})| 重绑还剩 ${json.device.rebindsLeft} 次` : '')
246
+ + (v.warnings || []).map((w) => `\n ⚠️ ${w}`).join('')
247
+ return { json, human, stderr: '', exit: Number(json.code) }
248
+ }
249
+
250
+ // ── install / renew(写类:默认 dry-run)────────────────────────────────────
251
+ function installLike(sub, args, deps) {
252
+ const p = parseSubArgs(sub, args)
253
+ if (!p.ok) return failJson(2, p.reason, p.error)
254
+ const file = flag(p.values['--file'])
255
+ if (sub === 'install' && !file) {
256
+ return failJson(2, 'usage_error', '缺必填参数 --file <许可文件>(用法:argent license install --file <许可> [--apply])')
257
+ }
258
+ if (file && !existsSync(resolve(file))) return failJson(2, 'usage_error', `--file 指向的许可文件不存在:${resolve(file)}`)
259
+ const confirm = CONFIRM_FLAGS.some((f) => p.bools.has(f))
260
+ const dryRun = !confirm || p.bools.has('--dry-run') // A3:默认 dry-run;`--dry-run` 恒优先
261
+
262
+ // `renew` 没有 --file ⇒ **成功但空**:本期唯一可用的续期路径是"换一份新许可文件",
263
+ // 在线端点属外部依赖(策划案②)。**绝不**把"没刷新成功"当成 denied(§3.5① 纪律 2)。
264
+ if (sub === 'renew' && !file) {
265
+ const v = licenseVerdict({ file: undefined })
266
+ const json = {
267
+ ok: true, empty: true, code: v.state === 'grace' ? 3 : 0,
268
+ reason: v.state === 'grace' ? 'license_grace' : 'no_refresh_source',
269
+ subcommand: 'renew',
270
+ licenseFile: v.path,
271
+ state: v.state,
272
+ expiresAt: v.expiresAt || null,
273
+ graceDays: v.graceDays,
274
+ graceUntil: v.graceUntil || null,
275
+ inGrace: v.inGrace === true,
276
+ device: deviceEcho(v.device),
277
+ refresh: {
278
+ requested: p.bools.has('--online'),
279
+ online: false,
280
+ attempted: false,
281
+ note: '本期没有自建在线续期端点(属外部依赖,见实施方案 Q3/策划案②);'
282
+ + '请用 `argent license renew --file <新许可>`(离线换新文件)或让服务端重签。'
283
+ + '**没有刷新成功 ≠ 许可被拒**:这里维持原状态并如实回报。',
284
+ },
285
+ warnings: v.warnings || [],
286
+ judgedBy: 'core/lib/license-verdict.mjs',
287
+ }
288
+ const human = `ℹ️ 没有可用的续期来源(no_refresh_source)⇒ 维持原状态:state=${json.state}`
289
+ + `\n ${json.refresh.note}`
290
+ return { json, human, stderr: '', exit: Number(json.code) }
291
+ }
292
+
293
+ const childArgs = ['--file', resolve(file), '--json']
294
+ if (p.values['--secret-file']) childArgs.push('--secret-file', resolve(p.values['--secret-file']))
295
+ if (p.bools.has('--rebind')) childArgs.push('--rebind')
296
+ // 本地重签(§3.6③ v2):参数**逐字透传**,判定仍只发生在 install-license.mjs 里
297
+ if (p.bools.has('--sign-v2')) childArgs.push('--sign-v2')
298
+ if (p.values['--sign-version']) childArgs.push('--sign-version', p.values['--sign-version'])
299
+ if (p.values['--device-code']) childArgs.push('--device-code', p.values['--device-code'])
300
+ if (p.bools.has('--device-from-local')) childArgs.push('--device-from-local')
301
+ if (p.bools.has('--no-device')) childArgs.push('--no-device')
302
+ // 装机/续期侧的**现役比对**守卫(2026-10-04):默认拒绝"比现役更早到期 / 能力席位被收窄",
303
+ // 显式 `--allow-regression` 才放行 —— 判定在 install-license.mjs 里,本层只透传(不复制判据)。
304
+ if (p.bools.has('--allow-regression')) childArgs.push('--allow-regression')
305
+ if (dryRun) childArgs.push('--dry-run')
306
+ const script = join(deps.repo, 'scripts', 'install-license.mjs')
307
+ const child = runNode(script, childArgs, deps)
308
+ const out = parseJson(child.stdout)
309
+ if (!out) {
310
+ return failJson(1, 'bad_output',
311
+ `install-license 输出不可解析(exit=${child.status}):${String(child.stderr || child.stdout).trim().slice(0, 300)}`)
312
+ }
313
+ // 退出码逐字透传(install-license 的口径:0 已安装/预演 | 1 验签或设备被拒 | 2 用法)
314
+ const code = child.status === 0 ? 0 : (child.status === 1 ? 1 : 2)
315
+ const json = {
316
+ ...out,
317
+ ok: code === 0,
318
+ empty: false,
319
+ code,
320
+ reason: code === 0 ? null : (out.reason || (code === 1 ? 'guard_rejected' : 'usage_error')),
321
+ subcommand: sub,
322
+ dryRun,
323
+ applied: code === 0 && !dryRun,
324
+ // 幂等(与现役**载荷+签名**逐字节一致 ⇒ 未重写、未备份)与现役比对(降级/缩权默认拒绝)
325
+ unchanged: out.unchanged === true,
326
+ incumbent: out.incumbent || null,
327
+ comparison: out.comparison || null,
328
+ renewal: out.renewal || null,
329
+ allowRegression: out.allowRegression === true,
330
+ licenseFile: out.source,
331
+ targetFile: out.target,
332
+ signatureVersion: out.signatureVersion ?? null,
333
+ deviceSigned: out.deviceSigned === true,
334
+ resign: out.resign || null,
335
+ device: out.device || null,
336
+ judgedBy: 'scripts/install-license.mjs',
337
+ }
338
+ const verb = sub === 'renew' ? '续期' : '安装'
339
+ const human = code === 0
340
+ ? `${dryRun ? '🔎 [dry-run] 会' : (json.unchanged ? '✅ 与现役一致、未改动,无需' : '✅ 已')}${verb}许可:${out.source} → ${out.target}`
341
+ + `\n 账号:${out.userId} | 席位:${out.seats} | 到期:${out.expiresAt}`
342
+ + `\n 签名版本:${out.signatureVersion === 2 ? 'v2(device 已进签名)' : (out.signatureVersion === 1 ? 'v1(device 未进签名)' : '判不出')}`
343
+ + (out.resign ? `\n ✍️ 重签:v${out.resign.from ?? '?'} → v${out.resign.to}` : '')
344
+ + (out.device ? `\n 设备:${out.device.binding}(${out.device.reason})| 重绑还剩 ${out.device.rebindsLeft} 次` : '')
345
+ + (json.unchanged ? '\n ♻️ 幂等:本文件与现役许可逐字节一致 ⇒ **未重写、未备份**(续期重放不再产生 churn)' : '')
346
+ + (dryRun ? `\n ⚠️ 本次是 dry-run:**零持久副作用**(不落许可、不备份、不写重绑计数)` : '')
347
+ : `❌ ${json.reason}:${out.error || ''}`
348
+ return { json, human, stderr: '', exit: code }
349
+ }
350
+
351
+ // ── request ────────────────────────────────────────────────────────────────
352
+ function requestVerb(sub, args, deps) {
353
+ const p = parseSubArgs(sub, args)
354
+ if (!p.ok) return failJson(2, p.reason, p.error)
355
+ const dev = currentDevice(deps.deviceRead ? { ...deps.deviceRead } : {})
356
+ const v = licenseVerdict({ deviceRead: deps.deviceRead || null, rebindStore: deps.rebindStore || null })
357
+ if (!dev.ok) {
358
+ // ⛔ 不崩、也**不误判为拒绝**:这是"本机不支持设备绑定"(环境),按 A1 用 2
359
+ return failJson(2, dev.reason,
360
+ `${dev.error}(设备绑定是**可选**能力:读不到指纹时许可按 unbound 放行,业务不受影响)`,
361
+ { subcommand: 'request', device: { code: '', algo: dev.algo, issuedAt: dev.issuedAt, supported: false } })
362
+ }
363
+ const storePath = rebindStoreInfo()
364
+ const json = {
365
+ ok: true,
366
+ empty: false,
367
+ code: 0,
368
+ reason: null,
369
+ subcommand: 'request',
370
+ // 可复制的**设备码**(离线激活第一步;服务端签发许可时写进载荷的 `device.code`)
371
+ device: { code: dev.code, algo: dev.algo, issuedAt: dev.issuedAt, supported: true },
372
+ // 本机现状(便于支持人员一眼看到"这台机器现在是什么状态")
373
+ license: {
374
+ state: v.state,
375
+ licenseFile: v.path,
376
+ present: v.reason !== 'missing' && v.reason !== 'unreadable',
377
+ binding: v.device ? v.device.binding : null,
378
+ bindingReason: v.device ? v.device.reason : null,
379
+ matchesMachine: dev.code === (v.device && v.device.code),
380
+ },
381
+ rebind: { used: Math.max(0, MAX_REBINDS - (v.device ? v.device.rebindsLeft : MAX_REBINDS)), max: MAX_REBINDS, graceDays: REBIND_GRACE_DAYS, store: storePath.path, storeMode: storePath.mode },
382
+ fingerprint: { algo: dev.algo, source: 'hardware-uuid(唯一来源,D3;实现见 core/lib/license-device.mjs)' },
383
+ warnings: ['设备码是**指纹摘要**(不含主机名/MAC/IP/安装路径)—— 只用于"这台机器 vs 许可声明"的相等比对'],
384
+ judgedBy: 'core/lib/license-device.mjs',
385
+ }
386
+ const human = `📇 本机设备码(复制给服务端签发许可用):\n ${dev.code}\n`
387
+ + ` 算法:${dev.algo} | 生成时间:${dev.issuedAt}\n`
388
+ + ` 当前许可:${json.license.state}(${json.license.binding || '无声明'})| 自助重绑已用 ${json.rebind.used}/${MAX_REBINDS} 次\n`
389
+ + ` ⚠️ 指纹只来自硬件 UUID(换机才变);${deviceSignedText({ signatureVersion: v.signatureVersion, device: v.device })}`
390
+ return { json, human, stderr: '', exit: 0 }
391
+ }
392
+
393
+ // ── explain ────────────────────────────────────────────────────────────────
394
+ function explainVerb(sub, args, deps) {
395
+ const p = parseSubArgs(sub, args)
396
+ if (!p.ok) return failJson(2, p.reason, p.error)
397
+ const st = statusVerb('status', args, deps)
398
+ if (st.json.reason === 'bad_output') return st
399
+ const j = st.json
400
+ const missing = []
401
+ const next = []
402
+ const dev = j.device || {}
403
+ if (j.licenseReason === 'missing') {
404
+ missing.push(`本机没有许可文件:${j.licenseFile}`)
405
+ next.push('把服务端签发的许可文件装上:argent license install --file <许可>(先 --dry-run 看一眼)')
406
+ } else if (j.licenseReason === 'no_secret') {
407
+ missing.push('本机没有验签密钥(license.secret / ARGENT_LICENSE_SECRET)')
408
+ next.push('让服务端把签名密钥一起交付,并在 install 时用 --secret-file 指定')
409
+ } else if (j.licenseReason === 'bad_signature') {
410
+ missing.push('许可验签失败(文件被改过,或不是本机这把密钥签的)')
411
+ next.push('别装这份文件:先确认传输是否损坏 / 是否拿错了密钥,再让服务端重签')
412
+ } else if (j.licenseReason === 'expired') {
413
+ missing.push('许可已过期且超出宽限期')
414
+ next.push('续期:argent license renew --file <新许可>(离线换新文件)')
415
+ } else if (j.licenseReason === 'malformed' || j.licenseReason === 'bad_version' || j.licenseReason === 'unreadable') {
416
+ missing.push(`许可文件不可用(${j.licenseReason})`)
417
+ next.push('重新装一份完整许可:argent license install --file <许可>')
418
+ }
419
+ if (j.gateReason === 'cap_not_allowed') {
420
+ missing.push(`许可里没有这次要用的能力(要 ${(j.requestedCaps || []).join(',')},许可只有 ${(j.capabilities || []).join(',') || '(空)'})`)
421
+ next.push('让服务端在许可里加上该能力(capabilities),或改跑已授权的流程')
422
+ }
423
+ if (j.gateReason === 'vertical_not_allowed') {
424
+ missing.push(`许可不含业务线 ${j.requestedVertical}(许可业务线:${(j.verticals || []).join(',') || '(空=不限)'})`)
425
+ next.push('让服务端把该业务线加进许可,或核对 --vertical 是否写错')
426
+ }
427
+ if (dev.binding === 'mismatch') {
428
+ missing.push(`设备码不匹配(本机 ${String(dev.machineCode || '').slice(0, 12)}… ≠ 许可 ${String(dev.code || '').slice(0, 12)}…)`)
429
+ next.push(`自助重绑:argent license install --file <许可> --rebind --apply(还剩 ${dev.rebindsLeft} 次,宽限 ${REBIND_GRACE_DAYS} 天)`)
430
+ next.push('次数用尽 ⇒ 需人工:让服务端按 `argent license request` 的设备码重签一份')
431
+ }
432
+ if (dev.binding === 'unsupported') {
433
+ missing.push('本机读不到设备指纹 ⇒ 这次**没有**做设备绑定校验(按 unbound 放行)')
434
+ next.push('若要启用绑定,换一台能读到硬件 UUID 的 macOS;否则无需处理(不 fail-closed)')
435
+ }
436
+ if (dev.binding === 'unbound') {
437
+ next.push('(可选)想启用设备绑定:argent license request 拿设备码 → 服务端签发带 device 的许可'
438
+ + '(本地演练:install-license --file <许可> --sign-v2 --device-code <本机设备码>)')
439
+ }
440
+ if (!missing.length) next.push('无需处理:许可有效(valid)')
441
+ const json = {
442
+ ok: j.ok,
443
+ empty: false,
444
+ code: j.code,
445
+ reason: j.reason,
446
+ subcommand: 'explain',
447
+ state: j.state,
448
+ licenseFile: j.licenseFile,
449
+ expiresAt: j.expiresAt,
450
+ graceDays: j.graceDays,
451
+ graceUntil: j.graceUntil,
452
+ inGrace: j.inGrace,
453
+ graceSource: j.graceSource,
454
+ signatureVersion: j.signatureVersion === 2 ? 2 : (j.signatureVersion === 1 ? 1 : null),
455
+ deviceSigned: j.device && j.device.signed === true,
456
+ device: j.device || null,
457
+ missing,
458
+ next,
459
+ whatIsMissing: missing, // 别名(文档 §3.5① 的行文用词)
460
+ warnings: j.warnings || [],
461
+ // §3.6③ 要求 explain **把局限说给人听**(文案**按签名版本动态化**:v2 不再说"未进签名")
462
+ limitation: {
463
+ device: deviceLimitationText(j),
464
+ rebind: `自助重绑上限 ${MAX_REBINDS} 次 + ${REBIND_GRACE_DAYS} 天宽限,计数落 $ARGENT_DATA_DIR`
465
+ + '(删掉计数文件即归零)⇒ 它是**礼貌上限**,不是可强制执行的限制。',
466
+ },
467
+ conclusion: j.conclusion || '',
468
+ judgedBy: j.judgedBy,
469
+ }
470
+ const human = `${j.ok ? (j.state === 'grace' ? '⚠️' : '✅') : '❌'} 许可解释:${j.state}(${j.reason || j.licenseReason || 'ok'})`
471
+ + (missing.length ? `\n 缺什么:\n${missing.map((m) => ` · ${m}`).join('\n')}` : '')
472
+ + `\n 下一步:\n${next.map((m) => ` · ${m}`).join('\n')}`
473
+ + `\n ⚠️ 局限:${json.limitation.device}`
474
+ return { json, human, stderr: '', exit: Number(json.code) }
475
+ }
476
+
477
+ /**
478
+ * verb 入口:`argent license <sub> …`。
479
+ * @returns {{json:object, human:string, stderr:string, exit:number}} 信封(CLI 负责打印与退出码)
480
+ */
481
+ export function runLicenseVerb(argv = [], deps = {}) {
482
+ const d = { repo: deps.repo || resolve(process.cwd()), env: deps.env || process.env, spawn: deps.spawn, ...deps }
483
+ // `--json` 是 **CLI 层**的开关(决定打印形状),不是子命令名 ⇒ 判定子命令时先滤掉它
484
+ const [sub, ...rest] = argv.filter((a) => a !== '--json')
485
+ // 裸 `argent license` = `status`(旧实现恒 ok:true/exit 0 正是 §2.2④ 要修的那件事)
486
+ if (!sub) return statusVerb('status', [], d)
487
+ if (sub === 'status') return statusVerb('status', rest, d)
488
+ if (sub === 'verify') return verifyVerb('verify', rest, d)
489
+ if (sub === 'install') return installLike('install', rest, d)
490
+ if (sub === 'request') return requestVerb('request', rest, d)
491
+ if (sub === 'renew') return installLike('renew', rest, d)
492
+ if (sub === 'explain') return explainVerb('explain', rest, d)
493
+ // `activate` 是**异步**的(真兑换要发 HTTP;CLI 顶层已 `await`,MCP 只走 status 的同步路径)
494
+ // `asJson` 往上带一层:`--json` 是本层滤掉的开关,而"标注必须被看见"这件事要靠它决定走 stdout 还是 stderr。
495
+ if (sub === 'activate') return activateVerb(rest, { ...d, asJson: argv.includes('--json') })
496
+ // `ops`(O2 只读运营巡检):同样是异步的(服务端模式要发 GET)—— CLI 顶层已 `await`。
497
+ // ⛔ 读类:没有 `--apply`,`applied` 恒 false;判据/口径在 `core/lib/license-ops.mjs`(本层只分派)。
498
+ if (sub === 'ops') return runOpsVerb(rest, { ...d, asJson: argv.includes('--json') })
499
+ if (sub === '--help' || sub === '-h') {
500
+ // 组帮助不是"用法错误":列出子命令后 exit 0(与 `flow --help` 同形)
501
+ const contracts = JSON.parse(readFileSync(join(d.repo, 'core', 'contracts', 'argent.contracts.json'), 'utf8'))
502
+ const subs = Object.keys(contracts.verbs.license.subcommands || {})
503
+ return {
504
+ json: { ok: true, empty: false, code: 0, reason: null, subcommand: 'license', subcommands: subs, usage: ACTIVATE_USAGE },
505
+ human: `argent license <子命令> —— ${contracts.verbs.license.desc}\n`
506
+ + Object.entries(contracts.verbs.license.subcommands || {}).map(([s, t]) => ` ${s.padEnd(8)} ${t}`).join('\n')
507
+ + `\n\n${ACTIVATE_USAGE}`
508
+ + '\n\n只读运营巡检(O2:在途码/失败码/事件时间线/席位账目/异常清单,替代 `mongosh`)用法见:'
509
+ + '`argent license ops --help`(⛔ 只读:不写库/不落审计,HTTP 只发 GET)',
510
+ stderr: '', exit: 0,
511
+ }
512
+ }
513
+ return failJson(2, 'unknown_subcommand',
514
+ `未知子命令:${sub}(可用:${LICENSE_SUBCOMMANDS.join('、')})`)
515
+ }
516
+
517
+ /**
518
+ * `device` 到底进没进签名 —— **给人和机器看的那一句话**(唯一文案落点,四个 verb + check-license 同口径)。
519
+ * 判据只有一处:`signatureVersion`(1|2|null)。⛔ 不在这里重算版本分派。
520
+ * @param {{signatureVersion?:number|null, device?:object|null}} r
521
+ */
522
+ export function deviceSignedText(r = {}) {
523
+ const dev = r.device || {}
524
+ if (r.signatureVersion === 2) {
525
+ return dev.present
526
+ ? '签名版本 **v2**:`device` **已进签名**(载荷 10 项,含 device{code,algo,issuedAt})⇒ 改它/摘它/降 v 重拼都会验签失败;'
527
+ + '⚠️ 签名仍是 HMAC-SHA256(对称密钥,验签方持同一把密钥)⇒ **不防"持有 license.secret 的人重签"**,真防伪需非对称签名'
528
+ : '签名版本 **v2**:本许可**未声明**设备(unbound)—— **缺位也被签名覆盖**(事后加 `device` 会验签失败)⇒ 不做比对、放行'
529
+ }
530
+ if (r.signatureVersion === 1) {
531
+ return '`device` **未进签名**(v1 只签 v,userId,capabilities,verticals,seats,issuedAt,expiresAt,graceDays,instanceHint 这 9 项)'
532
+ + '⇒ 它是"服务端声明 + 客户端比对",能防"误装到别的机器",**不防伪造**;'
533
+ + '要让它进签名需 v2(`install-license --sign-v2` 本地重签,或服务端升 v2 后签发,见 §6.2 A-⑨)。'
534
+ }
535
+ return '判不出签名版本(许可缺失 / 无密钥 / 文件不可解析)⇒ **本次没有**任何"device 已进签名"的保证'
536
+ }
537
+
538
+ /** `explain` 的 `limitation.device`:v2 的局限与 v1 **不同**(一个"已覆盖但仍是 HMAC",一个"根本没覆盖")。 */
539
+ function deviceLimitationText(j = {}) {
540
+ if (j.signatureVersion === 2) {
541
+ const base = '本许可的签名是 **v2**(载荷 10 项,含 `device{code,algo,issuedAt}`)'
542
+ + '⇒ `device` 的**内容或它的缺位**都被签名覆盖:改它/摘它/把 `v` 降回 1 重拼载荷,都会 **bad_signature**。'
543
+ return base + (j.device && j.device.present
544
+ ? '⚠️ 但这**不等于防伪**:签名算法是 HMAC-SHA256(对称密钥),验签方**必须**持有同一把 `license.secret`'
545
+ + '⇒ 拿到该文件的人能**自签一份**看起来完全合法的许可。v2 堵的是"改字段不改签名"这条最低成本绕过;'
546
+ + '要真防伪造得换非对称签名(Ed25519,客户端只拿公钥)—— v3 候选。'
547
+ : '⚠️ 本许可未声明设备(unbound):放行,但**没有**设备绑定这一层保护。')
548
+ }
549
+ if (j.signatureVersion === 1) {
550
+ return '`device` **未进签名**(v1 只签 v,userId,capabilities,verticals,seats,issuedAt,expiresAt,graceDays,instanceHint 这 9 项)'
551
+ + '⇒ 它是"服务端声明 + 客户端比对",能防"误装到别的机器",**不防伪造**;'
552
+ + '要让它进签名需服务端升 v2(跨仓库,见 §6.2 A-⑨)—— 本地可先 `install-license --file <许可> --sign-v2` 重签。'
553
+ }
554
+ return '本机这份许可**判不出签名版本**(缺失 / 无密钥 / 不合法)⇒ 没有任何"device 已进签名"的保证;'
555
+ + '在拿到 v2 许可之前,本机现状仍是 v1 口径:`device` **未进签名**(v1 只签 9 项),'
556
+ + '它是"服务端声明 + 客户端比对",**不防伪造**。'
557
+ }
558
+
559
+ /** 供门禁/宿主读的静态信息(**不复制判据**,只是清单)。 */
560
+ export const LICENSE_META = {
561
+ subcommands: LICENSE_SUBCOMMANDS,
562
+ confirmFlags: CONFIRM_FLAGS,
563
+ maxRebinds: MAX_REBINDS,
564
+ rebindGraceDays: REBIND_GRACE_DAYS,
565
+ }
566
+
567
+ /** 给测试/诊断用:直接读一份许可的判定结果(不 spawn 任何子进程)。 */
568
+ export function licenseStatusInProcess(opts = {}) {
569
+ const v = licenseVerdict(opts)
570
+ return {
571
+ state: v.state, exit: v.exit, ok: v.ok, reason: v.reason,
572
+ signatureVersion: v.signatureVersion === 2 ? 2 : (v.signatureVersion === 1 ? 1 : null),
573
+ deviceSigned: v.deviceSigned === true,
574
+ device: deviceEcho(v.device), readStore: readRebindStore({ env: opts.env || process.env }),
575
+ rawJson: (() => { try { return JSON.parse(readFileSync(v.path, 'utf8')) } catch { return null } })(),
576
+ }
577
+ }