@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,377 @@
1
+ #!/usr/bin/env python3
2
+ """lease —— 能力租约:客户端侧的授权闸门(批 C4,2026-09-21)。
3
+
4
+ 为什么需要(以及它为什么不是"防盗版"):
5
+ · 客户机上跑的是**本地能力包**,没有服务端依赖就跑得飞起 —— 于是"按能力订阅"只能靠
6
+ 装得上/装不上约束,买了什么全靠自觉。租约把"这一份能力现在还有效吗"变成一个
7
+ **我们自己签发的、有期限的凭据**,流程执行前查一次即可(轻量、可离线容忍)。
8
+ · 服务端 `POST /api/argent/lease` 签发(HMAC-SHA256,绑 instanceId + 能力清单 + 到期时间),
9
+ 本地**离线也要能验签** ⇒ 断网不影响在租期内的正常作业(客户机在客户内网里是常态)。
10
+ · 说明白边界:这是"锁",不是"保险柜"。拿不到租约时我们会**响亮停住并给人工指南**,
11
+ 而不是偷偷降级;也不会去删客户的数据或产物。
12
+
13
+ 判定(`--check <capability>` 的退出码):
14
+ 0 允许(租约有效或离线容忍期内)
15
+ 3 宽限(已过期但 ≤ graceUntil:允许跑,但**打醒目告警**并记台账,提示尽快恢复订阅)
16
+ 1 拒绝(超过宽限期 / 该能力不在租约里 / 没有租约且无法联网换新)
17
+ 2 参数或内部错误
18
+ 续订提醒(`--remind`)的**输出约定**:带 `⚠️`/`⛔` 才叫"要喊人"(`flow-trigger.sh` 就按这两个
19
+ 符号决定写 stamp / 台账 blocked / 推卡);不带 ⇒ 只是信息,触发器不产生任何外部动作。
20
+ 「完全没有租约 ∧ 闸门未启用(`lease.enforce=false`)」= 信息:缺租约本就被放行,喊人是噪音;
21
+ 闸门启用、或**租约存在但将到期/已过期** ⇒ 一律照旧告警(判据跟着开关走,不整段静音)。
22
+ 用法:
23
+ python3 scripts/lease.py --status # 看本地缓存的租约
24
+ python3 scripts/lease.py --refresh # 向服务端换一份新租约(需要 WHYSHU_API_KEY)
25
+ python3 scripts/lease.py --check full --flow full # 闸门(流程执行前调用)
26
+ """
27
+ from __future__ import annotations
28
+ import argparse, datetime, hashlib, hmac, json, os, pathlib, sys, time, urllib.error, urllib.request
29
+
30
+ try:
31
+ from flow_registry import consumer_doc as _registry_doc # P2a:base + 业务线合并(唯一真源)
32
+ except Exception: # noqa: BLE001 —— 半坏机器上退回单文件(升级链路要能跑)
33
+ def _registry_doc(path=None):
34
+ _p = pathlib.Path(path) if path else (HERE / "flow-requirements.json")
35
+ return json.loads(_p.read_text(encoding="utf-8"))
36
+
37
+
38
+ HERE = pathlib.Path(__file__).resolve().parent
39
+ sys.path.insert(0, str(HERE / 'lib'))
40
+ from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿在此复制链)
41
+ DATA = pathlib.Path(resolve_data_dir()['dir'])
42
+ LEASE_FILE = DATA / "lease.json"
43
+ INSTANCE_FILE = DATA / "instance.json"
44
+ EXIT_OK, EXIT_DENY, EXIT_ERR, EXIT_GRACE = 0, 1, 2, 3
45
+
46
+
47
+ def _secret() -> str:
48
+ return (os.environ.get("ARGENT_LEASE_SECRET")
49
+ or os.environ.get("INSTALL_TICKET_SECRET")
50
+ or "whyshu-install-ticket-secret")
51
+
52
+
53
+ def instance_id() -> str:
54
+ """机器身份:优先复用遥测的 installId(同一台机器的两个子系统必须同一身份)。
55
+
56
+ 兼容早期实现写过的其它键(机读为主,读不到就现生成一个并落盘 600)。
57
+ """
58
+ for key in ("instanceId", "installId", "id"):
59
+ try:
60
+ v = json.loads(INSTANCE_FILE.read_text(encoding="utf-8")).get(key)
61
+ if v:
62
+ return str(v)
63
+ except Exception: # noqa: BLE001
64
+ continue
65
+ iid = "inst_" + hashlib.sha256(f"{time.time()}".encode()).hexdigest()[:16]
66
+ DATA.mkdir(parents=True, exist_ok=True)
67
+ INSTANCE_FILE.write_text(json.dumps({"instanceId": iid}, ensure_ascii=False) + "\n", encoding="utf-8")
68
+ try:
69
+ os.chmod(INSTANCE_FILE, 0o600)
70
+ except Exception: # noqa: BLE001
71
+ pass
72
+ return iid
73
+
74
+
75
+ def _registry_lease() -> dict:
76
+ """注册表里的 `lease` 段(阈值口径的唯一出处;读不到返回空 dict)。"""
77
+ try:
78
+ doc = _registry_doc()
79
+ return doc.get("lease") or {}
80
+ except Exception: # noqa: BLE001
81
+ return {}
82
+
83
+
84
+ def api_base() -> str:
85
+ return (os.environ.get("WHYSHU_API_URL") or "https://whyshu.com").rstrip("/")
86
+
87
+
88
+ def api_token() -> str:
89
+ tok = os.environ.get("WHYSHU_API_KEY") or ""
90
+ if tok:
91
+ return tok
92
+ # 与遥测同源:credentials 落盘在 ~/.dsh/argent/*.env(600)
93
+ for name in ("whyshu.env", "fulfillment.env"):
94
+ p = DATA / name
95
+ try:
96
+ for line in p.read_text(encoding="utf-8").splitlines():
97
+ t = line.strip()
98
+ if t.startswith("WHYSHU_API_KEY="):
99
+ return t.split("=", 1)[1].strip().strip('"').strip("'")
100
+ except Exception: # noqa: BLE001
101
+ continue
102
+ return ""
103
+
104
+
105
+ def signature_of(lease: dict) -> str:
106
+ """**与服务端逐字一致**的签名串(改了这里必须同步改服务端与测试)。"""
107
+ caps = sorted(str(c) for c in (lease.get("capabilities") or []))
108
+ exp = int(lease.get("expiresAtMs") or lease.get("expiresAtMsParsed") or 0)
109
+ payload = f"{lease.get('leaseId')}.{lease.get('instanceId')}.{exp}.{','.join(caps)}"
110
+ return hmac.new(_secret().encode(), payload.encode(), hashlib.sha256).hexdigest()
111
+
112
+
113
+ def parse_lease(raw: dict) -> dict:
114
+ """服务端响应 → 本地缓存结构(时间统一存毫秒,便于离线判期)。"""
115
+ def ms(v):
116
+ """ISO 字符串 → 毫秒时间戳。**必须按 ISO 里的时区解析**:服务端给的是 `...Z`(UTC),
117
+ 早前用 `time.mktime`(本地时区)解析会整体偏移一个时区(CST 下 8 小时)——
118
+ 对 72h 租期只是几小时的错,但对短租期/宽限边界就是判错。"""
119
+ if isinstance(v, (int, float)):
120
+ return int(v)
121
+ try:
122
+ dt = datetime.datetime.fromisoformat(str(v).replace("Z", "+00:00"))
123
+ if dt.tzinfo is None:
124
+ dt = dt.astimezone() # 无时区标记 ⇒ 按本地解释
125
+ return int(dt.timestamp() * 1000)
126
+ except Exception: # noqa: BLE001
127
+ try:
128
+ return int(time.mktime(time.strptime(str(v)[:19], "%Y-%m-%dT%H:%M:%S")) * 1000)
129
+ except Exception: # noqa: BLE001
130
+ return 0
131
+ return {
132
+ "leaseId": str(raw.get("leaseId") or ""),
133
+ "instanceId": str(raw.get("instanceId") or instance_id()),
134
+ "plan": str(raw.get("plan") or ""),
135
+ "capabilities": [str(c) for c in (raw.get("capabilities") or [])],
136
+ "issuedAtMs": ms(raw.get("issuedAt")),
137
+ "expiresAtMs": ms(raw.get("expiresAt")),
138
+ "graceUntilMs": ms(raw.get("graceUntil")),
139
+ "signature": str(raw.get("signature") or ""),
140
+ "fetchedAt": int(time.time() * 1000),
141
+ }
142
+
143
+
144
+ def read_lease() -> dict:
145
+ try:
146
+ return json.loads(LEASE_FILE.read_text(encoding="utf-8"))
147
+ except Exception: # noqa: BLE001
148
+ return {}
149
+
150
+
151
+ def write_lease(lease: dict) -> None:
152
+ DATA.mkdir(parents=True, exist_ok=True)
153
+ tmp = LEASE_FILE.with_suffix(".tmp")
154
+ tmp.write_text(json.dumps(lease, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
155
+ os.replace(tmp, LEASE_FILE)
156
+ try:
157
+ os.chmod(LEASE_FILE, 0o600)
158
+ except Exception: # noqa: BLE001
159
+ pass
160
+
161
+
162
+ def refresh(ttl_hours: int = 0, caps: list | None = None) -> dict:
163
+ """向服务端换租约。失败**不抛**:返回 {'error': ...} 交给调用方决定(离线容忍)。"""
164
+ tok = api_token()
165
+ if not tok:
166
+ return {"error": "no-token", "hint": "缺 WHYSHU_API_KEY(~/.dsh/argent/whyshu.env 或进程环境)"}
167
+ body = {"instanceId": instance_id()}
168
+ if caps:
169
+ body["capabilities"] = caps
170
+ if ttl_hours:
171
+ body["ttlHours"] = int(ttl_hours)
172
+ req = urllib.request.Request(f"{api_base()}/api/argent/lease",
173
+ data=json.dumps(body).encode(),
174
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {tok}"},
175
+ method="POST")
176
+ try:
177
+ with urllib.request.urlopen(req, timeout=20) as r:
178
+ raw = json.loads(r.read().decode("utf-8") or "{}")
179
+ except urllib.error.HTTPError as e:
180
+ detail = ""
181
+ try:
182
+ detail = e.read().decode("utf-8")[:200]
183
+ except Exception: # noqa: BLE001
184
+ pass
185
+ return {"error": f"http-{e.code}", "detail": detail}
186
+ except Exception as e: # noqa: BLE001
187
+ return {"error": "offline", "detail": str(e)[:200]}
188
+ lease = parse_lease(raw)
189
+ if lease["leaseId"] and lease["expiresAtMs"]:
190
+ write_lease(lease)
191
+ return lease
192
+
193
+
194
+ def enforcement_on() -> bool:
195
+ """闸门是否真的拦人:注册表 `lease.enforce`(默认 false)+ 环境变量可临时打开。
196
+
197
+ 为什么要开关:能力包已经装在客户机上,贸然开闸会让正在跑的生产流程立刻停摆。
198
+ 上线顺序 = 部署服务端 → 客户端拿租约 → 开 enforce → 观察(先 grace 后 deny)。
199
+ """
200
+ env = os.environ.get("ARGENT_LEASE_ENFORCE")
201
+ if env is not None:
202
+ return env.strip().lower() in ("1", "true", "yes", "on")
203
+ try:
204
+ doc = _registry_doc()
205
+ return bool((doc.get("lease") or {}).get("enforce"))
206
+ except Exception: # noqa: BLE001
207
+ return False
208
+
209
+
210
+ def days_left(lease: dict | None = None, now_ms: int | None = None) -> float:
211
+ """租约还剩多少天(负数=已过期)。"""
212
+ le = lease if lease is not None else read_lease()
213
+ if not le:
214
+ return -999.0
215
+ now = int(now_ms if now_ms is not None else time.time() * 1000)
216
+ return (int(le.get("expiresAtMs") or 0) - now) / 86400000.0
217
+
218
+
219
+ def grace_left(lease: dict | None = None, now_ms: int | None = None) -> float:
220
+ """宽限还剩多少天(负数=宽限已过 ⇒ 该停跑)。"""
221
+ le = lease if lease is not None else read_lease()
222
+ if not le:
223
+ return -999.0
224
+ now = int(now_ms if now_ms is not None else time.time() * 1000)
225
+ return (int(le.get("graceUntilMs") or 0) - now) / 86400000.0
226
+
227
+
228
+ def needs_refresh(lease: dict | None = None, ahead_hours: float = 12.0, now_ms: int | None = None) -> bool:
229
+ """要不要现在去换一张新租约:没租约 / 已过期 / 距到期不足 ahead_hours。"""
230
+ le = lease if lease is not None else read_lease()
231
+ if not le or not le.get("leaseId"):
232
+ return True
233
+ now = int(now_ms if now_ms is not None else time.time() * 1000)
234
+ return (int(le.get("expiresAtMs") or 0) - now) <= ahead_hours * 3600 * 1000
235
+
236
+
237
+ def check(capability: str, lease: dict | None = None, now_ms: int | None = None) -> dict:
238
+ """闸门判定(纯函数):→ {verdict: ok|grace|deny, reason}。"""
239
+ now = int(now_ms if now_ms is not None else time.time() * 1000)
240
+ if not enforcement_on():
241
+ return {"verdict": "ok", "reason": "租约闸门未启用(注册表 lease.enforce=false / \$ARGENT_LEASE_ENFORCE)"}
242
+ le = lease if lease is not None else read_lease()
243
+ if not le or not le.get("leaseId"):
244
+ return {"verdict": "deny", "reason": "no-lease(本机还没有租约:跑一次 lease.py --refresh,"
245
+ "或确认安装时用了 --api-key)"}
246
+ want = signature_of(le)
247
+ if le.get("signature") and not hmac.compare_digest(str(le["signature"]), want):
248
+ return {"verdict": "deny", "reason": "签名不符(租约文件被改过或密钥不一致)⇒ 需重新 --refresh"}
249
+ if capability and capability not in (le.get("capabilities") or []):
250
+ return {"verdict": "deny", "reason": f"该能力不在租约里({capability} ∉ {le.get('capabilities')})"
251
+ f":套餐未覆盖或已变更"}
252
+ if now <= int(le.get("expiresAtMs") or 0):
253
+ return {"verdict": "ok", "reason": f"租约有效(至 {ms_str(le.get('expiresAtMs'))})"}
254
+ if now <= int(le.get("graceUntilMs") or 0):
255
+ return {"verdict": "grace", "reason": f"已过期但在宽限期内(至 {ms_str(le.get('graceUntilMs'))})"
256
+ f"⇒ 允许跑但请尽快恢复订阅"}
257
+ return {"verdict": "deny", "reason": f"租约与宽限期都已过(宽限至 {ms_str(le.get('graceUntilMs'))})"
258
+ f"⇒ 停跑该能力,联系问述科技续订"}
259
+
260
+
261
+ def ms_str(ms) -> str:
262
+ try:
263
+ return time.strftime("%Y-%m-%d %H:%M", time.localtime(int(ms) / 1000))
264
+ except Exception: # noqa: BLE001
265
+ return "?"
266
+
267
+
268
+ def main(argv=None) -> int:
269
+ ap = argparse.ArgumentParser(description="能力租约(客户端闸门)")
270
+ ap.add_argument("--status", action="store_true")
271
+ ap.add_argument("--refresh", action="store_true")
272
+ ap.add_argument("--check")
273
+ ap.add_argument("--flow", help="仅用于日志/台账(--check 时的流程名)")
274
+ ap.add_argument("--ttl-hours", type=int, default=0)
275
+ ap.add_argument("--refresh-if-needed", action="store_true",
276
+ help="只在快到期/没租约时才换租约(定时器每轮调用;不打扰服务端)")
277
+ ap.add_argument("--ahead-hours", type=float, default=0, help="--refresh-if-needed 的提前量(默认取注册表 lease.refreshAheadHours=12)")
278
+ ap.add_argument("--remind", action="store_true",
279
+ help="续订提醒:租约剩余不足注册表 lease.remindDays 时打印提醒(供触发器推飞书卡)")
280
+ ap.add_argument("--days", type=float, default=0, help="--remind 的天数阈值(默认取注册表)")
281
+ ap.add_argument("--json", action="store_true")
282
+ a = ap.parse_args(argv)
283
+
284
+ if a.refresh:
285
+ out = refresh(a.ttl_hours)
286
+ if a.json:
287
+ print(json.dumps(out, ensure_ascii=False))
288
+ elif out.get("error"):
289
+ print(f" ❌ 换租约失败:{out.get('error')} {out.get('detail') or out.get('hint') or ''}", file=sys.stderr)
290
+ return EXIT_ERR
291
+ else:
292
+ print(f" ✅ 新租约 {out['leaseId']}|plan={out['plan']}|能力={','.join(out['capabilities'])}"
293
+ f"|有效至 {ms_str(out['expiresAtMs'])}(宽限至 {ms_str(out['graceUntilMs'])})")
294
+ return EXIT_OK if not out.get("error") else EXIT_ERR
295
+
296
+ if a.refresh_if_needed:
297
+ ahead = a.ahead_hours or float((_registry_lease() or {}).get("refreshAheadHours") or 12)
298
+ if not needs_refresh(None, ahead):
299
+ if a.json:
300
+ print(json.dumps({"refreshed": False, "daysLeft": round(days_left(), 2)}, ensure_ascii=False))
301
+ return EXIT_OK
302
+ out = refresh()
303
+ if out.get("error"):
304
+ # 换不到**不是错误**(离线容忍):宽限期内继续跑,由 --remind / 触发器的检查负责喊人
305
+ print(f" ⚠️ 换租约失败({out.get('error')})——沿用旧租约,剩余 {days_left():.1f} 天"
306
+ + (f"(宽限 {grace_left():.1f} 天)" if grace_left() > 0 else ""), file=sys.stderr)
307
+ return EXIT_OK
308
+ print(f" ✅ 租约已续:{out['leaseId']}|有效至 {ms_str(out['expiresAtMs'])}({days_left(out):.1f} 天)")
309
+ return EXIT_OK
310
+
311
+ if a.remind:
312
+ le = read_lease()
313
+ days = a.days or float((_registry_lease() or {}).get("remindDays") or 7)
314
+ dl, gl = days_left(le), grace_left(le)
315
+ if not le:
316
+ # 「完全没有租约」**不是一种状态,而是两种**(2026-10-08 误报治理):
317
+ # · 闸门**开着**(lease.enforce=true / $ARGENT_LEASE_ENFORCE)⇒ 真问题:这台机器
318
+ # **跑不了**能力 ⇒ 照旧 ⚠️ 喊人续订(触发器据此写 stamp + 台账 blocked + 推卡)。
319
+ # · 闸门**关着**(默认 false)⇒ `check()` 第一条就 return ok,缺租约**由设计放行**
320
+ # (BYO/未登录问述/离线许可的机器就是这个常态)⇒ 再喊人是**纯噪音**:
321
+ # 触发器每天写一次 stamp + 台账 blocked + 推(推不出去的)卡,台账钩子又把
322
+ # blocked 采成 `needs_agent` 遥测 ⇒ 污染真失败信号(客户机实测每天一条,
323
+ # 10 条无一可处置)。故这里只打**普通信息**(无 ⚠️/⛔ ⇒ 触发器不会认成提醒)。
324
+ # 判据**必须跟着闸门开关走**:直接用闸门自己的 `enforcement_on()`(注册表 + 环境变量
325
+ # 同一条判据),不另立第二份;开关一打开,下一轮立即恢复喊人。
326
+ # 「租约存在但将到期/已过期」不受本分支影响(下面照旧告警)—— 不能整段静音。
327
+ if not enforcement_on():
328
+ print("ℹ️ 本机没有能力租约;但租约闸门未启用(注册表 lease.enforce=false)⇒ 缺租约按设计"
329
+ "放行,无需续订(要开始收租约:先跑 `lease.py --refresh`,再把 lease.enforce 改 true)")
330
+ return EXIT_OK
331
+ print("⚠️ 本机没有能力租约(跑 `lease.py --refresh` 或检查 WHYSHU_API_KEY)")
332
+ return EXIT_OK
333
+ if dl > days:
334
+ if a.json:
335
+ print(json.dumps({"daysLeft": round(dl, 2), "needRemind": False}, ensure_ascii=False))
336
+ return EXIT_OK
337
+ if dl >= 0:
338
+ print(f"⚠️ 能力租约将在 {dl:.1f} 天后到期({ms_str(le.get('expiresAtMs'))});"
339
+ f"若未自动续订,到期后 7 天宽限(至 {ms_str(le.get('graceUntilMs'))})内仍可跑,之后对应能力停跑")
340
+ else:
341
+ print(f"⛔ 能力租约已过期 {-dl:.1f} 天(宽限剩 {max(0.0, gl):.1f} 天,至 {ms_str(le.get('graceUntilMs'))})"
342
+ f"—— 请尽快续订,否则对应能力将被停跑")
343
+ return EXIT_OK
344
+
345
+ if a.status:
346
+ le = read_lease()
347
+ if not le:
348
+ print(" 本机还没有租约(跑 --refresh)" if not a.json else json.dumps({"lease": None}, ensure_ascii=False))
349
+ return EXIT_OK
350
+ v = check("", le)
351
+ if a.json:
352
+ print(json.dumps({"lease": le, "verdict": v["verdict"], "reason": v["reason"],
353
+ "instanceId": instance_id(),
354
+ "daysLeft": round(days_left(le), 2), "graceDaysLeft": round(grace_left(le), 2)},
355
+ ensure_ascii=False, indent=2))
356
+ else:
357
+ print(f" 租约 {le.get('leaseId')}|plan={le.get('plan')}|能力={','.join(le.get('capabilities') or [])}")
358
+ print(f" 有效至 {ms_str(le.get('expiresAtMs'))}(剩 {days_left(le):.1f} 天)"
359
+ f"|宽限至 {ms_str(le.get('graceUntilMs'))}(剩 {grace_left(le):.1f} 天)"
360
+ f"|判定 {v['verdict']}:{v['reason']}")
361
+ return EXIT_OK
362
+
363
+ if a.check:
364
+ v = check(a.check)
365
+ if a.json:
366
+ print(json.dumps({"capability": a.check, "flow": a.flow, **v}, ensure_ascii=False))
367
+ else:
368
+ icon = {"ok": "✅", "grace": "⚠️", "deny": "⛔"}[v["verdict"]]
369
+ print(f" {icon} 能力租约({a.check}{'/' + a.flow if a.flow else ''}):{v['reason']}")
370
+ return {"ok": EXIT_OK, "grace": EXIT_GRACE, "deny": EXIT_DENY}[v["verdict"]]
371
+
372
+ ap.print_help()
373
+ return EXIT_OK
374
+
375
+
376
+ if __name__ == "__main__":
377
+ raise SystemExit(main())