foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,95 @@
1
+ """
2
+ 统一规划原语(P1-2:三个 Planner 合并)测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. 共享原语:is_negated / split_on / keyword_hits(否定开关)
6
+ 2. trigger_route 引擎:否定过滤、打分、兜底、near-ties→needs_composition、确定性平局
7
+ 3. 三处委托同一引擎(单一真源):
8
+ · AHS pipeline.route 否定生效(不选被否定的 skill)
9
+ · search QueryPlanner._decompose 用共享拆分
10
+ · 主管关键词命中走 keyword_hits
11
+ 4. 结构:pipeline 不再持有本地 _negated/_split_on(已删除,证明去重)
12
+ """
13
+ import os, sys
14
+ from pathlib import Path
15
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ from foxclaw.planning import planner as P
19
+
20
+
21
+ def test_primitives():
22
+ print("\n【1】共享原语:否定 / 拆分 / 关键词命中")
23
+ assert P.is_negated("不要计算这个", "计算") is True
24
+ assert P.is_negated("请计算这个", "计算") is False
25
+ assert P.split_on("查北京,查上海、查广州", P.LIST_SEPS) == ["查北京", "查上海", "查广州"]
26
+ assert P.split_on("先做A 然后 做B", P.SPLIT_MARKERS) == ["先做A", "做B"]
27
+ assert P.keyword_hits("分析财务收支", ["财务", "收支", "行程"]) == 2
28
+ assert P.keyword_hits("不要分析财务", ["财务"], negation=True) == 0
29
+ assert P.keyword_hits("不要分析财务", ["财务"], negation=False) == 1
30
+ print(" ✓ is_negated / split_on / keyword_hits(否定开关) 均正确")
31
+
32
+
33
+ def test_trigger_route_engine():
34
+ print("\n【2】trigger_route 引擎:否定/打分/兜底/near-ties/确定性")
35
+ C = P.ScoredCandidate
36
+ cands = [
37
+ C("calculator", ["计算", "算"], "四则运算 计算器"),
38
+ C("web", ["搜索", "查"], "联网 搜索 网页"),
39
+ C("email", ["发邮件", "email"], "发送 邮件"),
40
+ ]
41
+ # 否定:"不要计算" → calculator 触发被否定 → 不选它
42
+ r = P.trigger_route("不要计算一下", cands)
43
+ assert r.primary != "calculator", f"否定应排除 calculator,实得 {r.primary}"
44
+ # 命中:"帮我搜索北京" → web
45
+ r2 = P.trigger_route("帮我搜索北京", cands)
46
+ assert r2.primary == "web" and r2.confidence >= 0.15
47
+ # 兜底:完全不相关 → 用最后一个候选兜底,低置信
48
+ r3 = P.trigger_route("xyzzy 无关内容", cands)
49
+ assert r3.primary == cands[-1].id and r3.confidence < 0.15
50
+ # 确定性:空候选
51
+ assert P.trigger_route("x", []).primary is None
52
+ print(f" ✓ 否定排除 / 命中选中 / 兜底(={r3.primary}) / 空候选")
53
+
54
+
55
+ def test_three_sites_delegate():
56
+ print("\n【3】三处委托同一引擎(单一真源)")
57
+ # AHS
58
+ from foxclaw.skills import build_default_registry
59
+ from foxclaw.skills.pipeline import route, plan
60
+ reg = build_default_registry()
61
+ assert route("不要计算", reg).primary != "calculator", "AHS route 否定生效"
62
+ assert len(plan("先算 5+5 然后再算 9*9", reg).tasks) == 2, "AHS plan 拆解"
63
+ # search
64
+ from foxclaw.search.planner import QueryPlanner
65
+ sub = QueryPlanner()._decompose("查A,查B、查C")
66
+ assert len(sub) == 3, "search 拆解走共享原语"
67
+ # supervisor(用引擎的 keyword_hits;此处直接验证原语等价)
68
+ assert P.keyword_hits("买基金理财", ["理财", "基金", "收支"]) == 2
69
+ print(" ✓ AHS / search / 主管 三处均由 foxclaw.planning 单一真源驱动")
70
+
71
+
72
+ def test_dedup_structural():
73
+ print("\n【4】结构去重:pipeline 不再持有本地 _negated/_split_on")
74
+ import foxclaw.skills.pipeline as pipe
75
+ assert not hasattr(pipe, "_negated"), "pipeline 应删除本地 _negated(改用共享)"
76
+ assert not hasattr(pipe, "_split_on"), "pipeline 应删除本地 _split_on(改用共享)"
77
+ assert not hasattr(pipe, "_NEGATIONS"), "pipeline 应删除本地 _NEGATIONS 常量"
78
+ print(" ✓ pipeline 的重复否定/拆分实现已删除,收敛到 foxclaw.planning")
79
+
80
+
81
+ def main():
82
+ print("=" * 60)
83
+ print("统一规划原语 · 三个 Planner 合并(P1-2)")
84
+ print("=" * 60)
85
+ test_primitives()
86
+ test_trigger_route_engine()
87
+ test_three_sites_delegate()
88
+ test_dedup_structural()
89
+ print("\n" + "=" * 60)
90
+ print("✓ P1-2 验证完成:一个规划引擎,主管/AHS/search 三处委托")
91
+ print("=" * 60)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -0,0 +1,109 @@
1
+ """
2
+ 统一准入门(PolicyGate, v2 ① Step4)测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. 推理准入归一化:Harness PASS/WARN/BLOCK → Verdict allow/warn/deny(raw 透传同一 HarnessResult)
6
+ 2. 动作准入归一化:permission allow/ask/deny → Verdict allow/confirm/deny(raw 透传);
7
+ 非 allow 发统一 policy.decision.* 事件,allow 不发(降噪)
8
+ 3. 真实集成:经 Foxclaw,技能路径(副作用 skill 需确认)发出 policy.decision 事件
9
+ """
10
+ import os, sys, asyncio, tempfile, shutil
11
+ from pathlib import Path
12
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
13
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
14
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ from foxclaw.kernel import PolicyGate
18
+
19
+
20
+ class _HR: # 假 HarnessResult
21
+ def __init__(self, blocked=False, warned=False, reason=""):
22
+ self._b, self._w, self.reason = blocked, warned, reason
23
+ @property
24
+ def is_blocked(self): return self._b
25
+ @property
26
+ def is_warned(self): return self._w
27
+
28
+
29
+ class _Harness:
30
+ def __init__(self, hr): self.hr = hr; self.calls = 0
31
+ def check(self, node): self.calls += 1; return self.hr
32
+
33
+
34
+ class _PD: # 假 PermissionDecision
35
+ def __init__(self, decision, reason=""):
36
+ self.decision, self.stage, self.reason, self.trust_downgrade = decision, 2, reason, False
37
+
38
+
39
+ class _CapSpine:
40
+ def __init__(self): self.events = []
41
+ def publish(self, topic, payload=None, source="", **kw): self.events.append(topic)
42
+
43
+
44
+ class _Skill:
45
+ id = "gmail_send"
46
+
47
+
48
+ def test_reasoning_normalization():
49
+ print("\n【1】推理准入归一化(Harness → Verdict;raw 透传)")
50
+ gate = PolicyGate()
51
+ h = _Harness(_HR())
52
+ v = gate.check_reasoning(h, None)
53
+ assert v.level == "allow" and v.allowed and v.raw is h.hr and h.calls == 1
54
+ v = gate.check_reasoning(_Harness(_HR(warned=True)), None)
55
+ assert v.level == "warn" and v.allowed
56
+ v = gate.check_reasoning(_Harness(_HR(blocked=True, reason="越界")), None)
57
+ assert v.level == "deny" and not v.allowed and v.reason == "越界"
58
+ print(" ✓ PASS→allow / WARN→warn / BLOCK→deny;raw 仍是同一 HarnessResult(行为一致)")
59
+
60
+
61
+ def test_action_normalization_and_event():
62
+ print("\n【2】动作准入归一化 + 非 allow 发 policy.decision")
63
+ for decision, level, event in [("allow", "allow", None),
64
+ ("ask", "confirm", "policy.decision.confirm"),
65
+ ("deny", "deny", "policy.decision.deny")]:
66
+ spine = _CapSpine()
67
+ pd = _PD(decision, reason=f"r-{decision}")
68
+ gate = PolicyGate(spine=spine, action_evaluator=lambda *a, _pd=pd, **k: _pd)
69
+ v = gate.check_action(_Skill(), {})
70
+ assert v.level == level and v.raw is pd, f"{decision}→{v.level}"
71
+ assert v.allowed == (decision == "allow")
72
+ assert spine.events == ([event] if event else []), (decision, spine.events)
73
+ print(" ✓ allow(不发) / ask→confirm(发) / deny(发);raw 透传 PermissionDecision")
74
+
75
+
76
+ def test_integration_skill_path():
77
+ print("\n【3】真实集成:技能路径经统一门,副作用 skill 发 policy.decision")
78
+ d = tempfile.mkdtemp(prefix="pg_")
79
+ try:
80
+ from foxclaw import Foxclaw
81
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
82
+ seen = []
83
+ os_.event_spine.project(lambda e: seen.append(e.topic))
84
+ # 发邮件是副作用 skill → 需确认 → 经 PolicyGate 发 policy.decision.confirm
85
+ os_.supervisor.use_skill("帮我发邮件给客户",
86
+ args={"to": "a@b.com", "subject": "x", "body": "y"})
87
+ assert any(t.startswith("policy.decision") for t in seen), \
88
+ f"技能准入应发 policy.decision,实得 {[t for t in seen if 'policy' in t]}"
89
+ assert os_.capabilities.get("policy_gate") is os_.policy_gate
90
+ print(f" ✓ use_skill(副作用)→ 发 {[t for t in seen if t.startswith('policy')]}")
91
+ asyncio.run(os_.aclose())
92
+ finally:
93
+ shutil.rmtree(d, ignore_errors=True)
94
+
95
+
96
+ def main():
97
+ print("=" * 60)
98
+ print("统一准入门 · PolicyGate(v2 ① Step4)")
99
+ print("=" * 60)
100
+ test_reasoning_normalization()
101
+ test_action_normalization_and_event()
102
+ test_integration_skill_path()
103
+ print("\n" + "=" * 60)
104
+ print("✓ Step4 验证完成:两个门统一到 Verdict 词汇 + 一个接口 + 统一审计")
105
+ print("=" * 60)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,185 @@
1
+ """
2
+ 领域认知化(v2 ③ Step1:DomainReasoner + 财务守门人形式化)测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. DomainReasoner 基座:reason() 按序 observe→hypothesize→gather_evidence→arbitrate
6
+ 2. FinanceGatekeeperReasoner:小额→approve、大额→confirm、低余额→reject(freeze);
7
+ 裁决与原守门人逐字一致 + 显式证据链(source=rules) + 假设
8
+ 3. 真实路径:orchestrator.route_request 走认知化守门人,req 附证据链(可审计)
9
+ """
10
+ import os, sys, asyncio, tempfile, shutil
11
+ from pathlib import Path
12
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
13
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
14
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ from foxclaw.cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
18
+
19
+
20
+ def test_reasoner_base_order():
21
+ print("\n【1】DomainReasoner 基座:四步按序执行")
22
+ order = []
23
+ class R(DomainReasoner):
24
+ def observe(self, s): order.append("observe"); return {"x": s}
25
+ def hypothesize(self, o): order.append("hypothesize"); return ["h"]
26
+ def gather_evidence(self, s, o): order.append("evidence"); return [Evidence("f", "d")]
27
+ def arbitrate(self, s, o, h, e):
28
+ order.append("arbitrate")
29
+ return ReasonedVerdict("approve", "ok", e, h, "rules")
30
+ rv = R().reason(42)
31
+ assert order == ["observe", "hypothesize", "evidence", "arbitrate"], order
32
+ assert rv.decision == "approve" and rv.hypotheses == ["h"] and len(rv.evidence) == 1
33
+ print(" ✓ observe→hypothesize→gather_evidence→arbitrate;证据链随裁决返回")
34
+
35
+
36
+ def _policy():
37
+ from foxclaw.finance import FinanceRuntime
38
+ # data_path 显式隔离:默认落在 policy 同目录(系统 Temp),会与其他测试共用一份数据文件
39
+ p = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
40
+ data_path=tempfile.mktemp(suffix=".json")).policy
41
+ # 显式锁定本测试的前提阈值(200,模糊带 [140,200)),**不依赖产品默认值** ——
42
+ # 默认阈值是给用户按自己实际调的可调项,不该让调它就牵动这些验证推理逻辑的测试。
43
+ for r in p.rules:
44
+ if r.id == "large_purchase_check":
45
+ r.threshold = 200.0
46
+ r.condition = "单笔支出 > 200"
47
+ return p
48
+
49
+
50
+ def test_finance_gatekeeper_reasoner():
51
+ print("\n【2】财务守门人认知化:裁决逐字一致 + 证据链 + 假设")
52
+ from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
53
+ from foxclaw.finance.models import Transaction
54
+ r = FinanceGatekeeperReasoner(_policy())
55
+ # 小额(规则内)→ approve
56
+ rv = r.reason((Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
57
+ assert rv.decision == "approve" and rv.rationale == "财务批准:travel 预算 150 在规则内"
58
+ assert rv.source == "rules" and any(e.factor == "rules_clear" for e in rv.evidence)
59
+ assert rv.hypotheses, "应有假设"
60
+ # 大额(>阈值)→ confirm,证据=large_purchase_check
61
+ rv2 = r.reason((Transaction(amount=-680, category="travel", entity="酒店", account="default"), 5000))
62
+ assert rv2.decision == "confirm" and any(e.factor == "large_purchase_check" for e in rv2.evidence)
63
+ print(f" ✓ 小额→approve(证据 rules_clear);大额→confirm(证据 large_purchase_check;假设={rv2.hypotheses})")
64
+
65
+
66
+ def test_context_evidence_no_decision_change():
67
+ print("\n【2b】Step2:情境证据(历史/预算/频率)丰满证据链,但**决策不变**")
68
+ from foxclaw.finance import FinanceRuntime
69
+ from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
70
+ from foxclaw.finance.models import Transaction
71
+ rt = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
72
+ data_path=tempfile.mktemp(suffix=".json"))
73
+ rt.set_budget("travel", 3000); rt.set_balance("default", 5000)
74
+ for _ in range(3): # 3 笔历史同类交易(同对手方)
75
+ rt.add_transaction(Transaction(amount=-100, category="travel", entity="高铁",
76
+ account="default"), run_gatekeeper=False)
77
+ r = FinanceGatekeeperReasoner(rt.policy, runtime=rt)
78
+ rv = r.reason((Transaction(amount=-150, category="travel", entity="高铁",
79
+ account="default"), 5000))
80
+ assert rv.decision == "approve", "规则内 → 决策仍是 approve(情境证据不改裁决)"
81
+ factors = {e.factor for e in rv.evidence}
82
+ assert {"history_category", "budget_remaining", "entity_frequency"} <= factors, factors
83
+ # 无 runtime → 无情境证据(向后兼容;裁决相同)
84
+ rv0 = FinanceGatekeeperReasoner(rt.policy).reason(
85
+ (Transaction(amount=-150, category="travel", entity="高铁", account="default"), 5000))
86
+ assert not ({"history_category", "budget_remaining", "entity_frequency"}
87
+ & {e.factor for e in rv0.evidence})
88
+ assert rv0.decision == rv.decision == "approve"
89
+ print(f" ✓ 决策 approve 不变;证据链含 {sorted(factors & {'history_category','budget_remaining','entity_frequency'})}")
90
+
91
+
92
+ class _MockLLM:
93
+ def __init__(self, resp="APPROVE", raises=False):
94
+ self.resp, self.raises, self.calls, self.is_stub = resp, raises, 0, False
95
+ def complete(self, system, messages, max_tokens=2048, agent_id=""):
96
+ self.calls += 1
97
+ if self.raises:
98
+ raise RuntimeError("llm down")
99
+ return self.resp
100
+
101
+
102
+ def test_llm_arbitration_safety():
103
+ print("\n【2c】Step3:LLM 裁决的安全不变量(只增强模糊、只能 approve→confirm、永不 reject)")
104
+ from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
105
+ from foxclaw.finance.models import Transaction
106
+ policy = _policy() # large_purchase_check 阈值 200;模糊带 [140,200)
107
+ def txn(amt): return Transaction(amount=-amt, category="travel", entity="高铁", account="default")
108
+ def mk(llm, enabled=True): return FinanceGatekeeperReasoner(policy, llm=llm, llm_enabled=enabled)
109
+
110
+ # 1. 规则触发(680>200)→ 规则权威,LLM 绝不被调
111
+ llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(680), 5000))
112
+ assert rv.decision == "confirm" and rv.source == "rules" and llm.calls == 0
113
+
114
+ # 2. 模糊(150∈[140,200))+ LLM 说 CONFIRM → confirm, source=llm
115
+ llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(150), 5000))
116
+ assert rv.decision == "confirm" and rv.source == "llm" and llm.calls == 1
117
+
118
+ # 3. 模糊 + LLM 说 APPROVE → approve, source=llm
119
+ llm = _MockLLM("APPROVE"); rv = mk(llm).reason((txn(150), 5000))
120
+ assert rv.decision == "approve" and rv.source == "llm"
121
+
122
+ # 4. LLM 出错/超时 → confirm(fail-safe),绝不自动放行
123
+ llm = _MockLLM(raises=True); rv = mk(llm).reason((txn(150), 5000))
124
+ assert rv.decision == "confirm" and rv.source == "llm_failsafe"
125
+
126
+ # 5. 【核心不变量】LLM 乱回(含"拒绝"/REJECT/含糊)→ **绝不 reject**,fail-safe confirm
127
+ for bad in ["REJECT", "让我拒绝这笔", "不确定", "yes maybe"]:
128
+ llm = _MockLLM(bad); rv = mk(llm).reason((txn(150), 5000))
129
+ assert rv.decision == "confirm" and rv.source == "llm_failsafe", (bad, rv.decision)
130
+ assert rv.decision != "reject", "LLM 永远不能产生 reject"
131
+
132
+ # 6. 明显安全(50<140)→ 规则批准,LLM 不调(省成本)
133
+ llm = _MockLLM("CONFIRM"); rv = mk(llm).reason((txn(50), 5000))
134
+ assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
135
+
136
+ # 7. 默认关(llm_enabled=False)→ 全走规则,LLM 不调(behavior-preserving)
137
+ llm = _MockLLM("CONFIRM"); rv = mk(llm, enabled=False).reason((txn(150), 5000))
138
+ assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
139
+
140
+ # 8. is_stub(无真实模型)→ 视为不可用,不调 LLM
141
+ stub = _MockLLM("CONFIRM"); stub.is_stub = True
142
+ rv = FinanceGatekeeperReasoner(policy, llm=stub, llm_enabled=True).reason((txn(150), 5000))
143
+ assert rv.source == "rules" and stub.calls == 0
144
+ print(" ✓ 规则权威不问LLM · 模糊只 approve/confirm · 出错/乱回 fail-safe confirm · "
145
+ "永不 reject · 明显安全/默认关/stub 不调")
146
+
147
+
148
+ def test_real_path_evidence():
149
+ print("\n【3】真实路径:认知化守门人 + req 附证据链")
150
+ d = tempfile.mkdtemp(prefix="reasoner_")
151
+ try:
152
+ from foxclaw import Foxclaw
153
+ from foxclaw.orchestration import CrossAgentRequest
154
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
155
+ os_.finance_runtime.set_balance("default", 5000)
156
+ # 大额 → 需确认(裁决不变)+ 证据链附在 req 上
157
+ req = os_.orchestrator.route_request(CrossAgentRequest(
158
+ from_agent="admin", to_agent="finance", request_type="budget_request",
159
+ payload={"amount": 680, "category": "travel", "entity": "酒店", "account": "default"},
160
+ reason="大额"))
161
+ assert req.status == "needs_confirmation", req.status
162
+ assert hasattr(req, "evidence") and req.evidence, "守门人应附证据链"
163
+ assert req.reason_source == "rules"
164
+ print(f" ✓ status={req.status}(裁决不变);evidence={req.evidence};source={req.reason_source}")
165
+ asyncio.run(os_.aclose())
166
+ finally:
167
+ shutil.rmtree(d, ignore_errors=True)
168
+
169
+
170
+ def main():
171
+ print("=" * 60)
172
+ print("领域认知化 · DomainReasoner + 财务守门人(v2 ③ Step1)")
173
+ print("=" * 60)
174
+ test_reasoner_base_order()
175
+ test_finance_gatekeeper_reasoner()
176
+ test_context_evidence_no_decision_change()
177
+ test_llm_arbitration_safety()
178
+ test_real_path_evidence()
179
+ print("\n" + "=" * 60)
180
+ print("✓ Step1 验证完成:观察→假设→证据→裁决 形式化;规则实现、裁决不变、证据链可审计")
181
+ print("=" * 60)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
@@ -0,0 +1,263 @@
1
+ """发布前校验脚本(tools/release_check.py)的规则测试。
2
+
3
+ 锁住三件事:
4
+ 1. 四处版本号必须一致 —— 只改一处的发布物会让用户装到对不上的版本;
5
+ 2. tag 必须指向本次版本 —— 发布不可撤回,yank 也仍占号;
6
+ 3. 产物里不能有 .env / docs/internal / 运行期数据,缺 vendor 只算提示(占位包现状)。
7
+
8
+ 脚本放在 tools/ 不在包里,故按文件路径加载;测试只构造最小归档,不跑真构建(秒级)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import io
15
+ import json
16
+ import shutil
17
+ import sys
18
+ import tarfile
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ REPO = Path(__file__).resolve().parents[1]
25
+
26
+
27
+ def _load():
28
+ spec = importlib.util.spec_from_file_location(
29
+ "release_check", REPO / "tools" / "release_check.py")
30
+ mod = importlib.util.module_from_spec(spec)
31
+ sys.modules["release_check"] = mod # dataclass 需要能按模块名回查,否则 exec 时炸
32
+ spec.loader.exec_module(mod)
33
+ return mod
34
+
35
+
36
+ rc = _load()
37
+
38
+
39
+ # ── 造最小产物 ────────────────────────────────────────────
40
+
41
+ def _wheel(dist: Path, version="0.1.0", name="foxclaw", extra=(),
42
+ description=True) -> Path:
43
+ p = dist / f"{name}-{version}-py3-none-any.whl"
44
+ body = ("\n\n" + "long description body. " * 12) if description else ""
45
+ with zipfile.ZipFile(p, "w") as z:
46
+ z.writestr(f"{name}/__init__.py", "__version__ = '0.1.0'\n")
47
+ z.writestr(f"{name}/cli.py", "def main(): pass\n")
48
+ z.writestr(f"{name}-{version}.dist-info/METADATA",
49
+ f"Name: {name}\nVersion: {version}{body}\n")
50
+ for e in extra:
51
+ z.writestr(e, "x")
52
+ return p
53
+
54
+
55
+ def _sdist(dist: Path, version="0.1.0", name="foxclaw", extra=()) -> Path:
56
+ p = dist / f"{name}-{version}.tar.gz"
57
+ with tarfile.open(p, "w:gz") as t:
58
+ for arc in (f"{name}-{version}/foxclaw/cli.py",
59
+ f"{name}-{version}/PKG-INFO", *extra):
60
+ data = b"x"
61
+ info = tarfile.TarInfo(arc)
62
+ info.size = len(data)
63
+ t.addfile(info, io.BytesIO(data))
64
+ return p
65
+
66
+
67
+ def _tgz(dist: Path, version="0.1.0", name="foxclaw", pkg=None, extra=()) -> Path:
68
+ p = dist / f"{name}-{version}.tgz"
69
+ payload = json.dumps(pkg if pkg is not None else
70
+ {"name": name, "version": version}).encode()
71
+ with tarfile.open(p, "w:gz") as t:
72
+ for arc, data in (("package/package.json", payload),
73
+ ("package/index.js", b"export default {}"),
74
+ *[(e, b"x") for e in extra]):
75
+ info = tarfile.TarInfo(arc)
76
+ info.size = len(data)
77
+ t.addfile(info, io.BytesIO(data))
78
+ return p
79
+
80
+
81
+ def _mini_repo(root: Path, version: str, npm_version: str) -> Path:
82
+ """只含四处版本来源的最小仓库,用来测"漏改一处"。"""
83
+ (root / "foxclaw").mkdir(parents=True)
84
+ (root / "npm").mkdir()
85
+ (root / "pyproject.toml").write_text(
86
+ f'[project]\nname = "foxclaw"\nversion = "{version}"\n', encoding="utf-8")
87
+ (root / "foxclaw" / "__init__.py").write_text(
88
+ f'__version__ = "{version}"\n', encoding="utf-8")
89
+ (root / "npm" / "package.json").write_text(
90
+ json.dumps({"name": "foxclaw", "version": npm_version}), encoding="utf-8")
91
+ (root / "npm" / "index.js").write_text(
92
+ f'export const version = "{npm_version}";\n', encoding="utf-8")
93
+ return root
94
+
95
+
96
+ # ── 版本与 tag ────────────────────────────────────────────
97
+
98
+ def test_repo_versions_are_consistent():
99
+ """真仓库当前状态:四处一致,无 error。"""
100
+ assert [i for i in rc.check_versions(REPO) if i.level == "error"] == []
101
+
102
+
103
+ def test_single_missed_source_is_an_error(tmp_path):
104
+ root = _mini_repo(tmp_path, "0.2.0", npm_version="0.1.0")
105
+ errs = [i for i in rc.check_versions(root) if i.level == "error"]
106
+ assert errs and any("npm" in i.msg for i in errs), errs
107
+
108
+
109
+ def test_unreadable_source_is_an_error(tmp_path):
110
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
111
+ (root / "npm" / "index.js").write_text("// 没有导出\n", encoding="utf-8")
112
+ assert any(i.code == "unreadable" for i in rc.check_versions(root))
113
+
114
+
115
+ @pytest.mark.parametrize("tag", ["v0.1.0", "0.1.0"])
116
+ def test_tag_matching_version_passes(tag):
117
+ assert rc.check_tag(tag, "0.1.0") == []
118
+
119
+
120
+ @pytest.mark.parametrize("tag", ["v0.2.0", "v0.1.0-rc1"])
121
+ def test_tag_drift_is_an_error(tag):
122
+ errs = rc.check_tag(tag, "0.1.0")
123
+ assert errs and errs[0].code == "tag_mismatch"
124
+
125
+
126
+ def test_empty_tag_is_skipped():
127
+ assert rc.check_tag("", "0.1.0") == []
128
+
129
+
130
+ # ── 项目页正文(long_description)────────────────────────────
131
+
132
+ def test_repo_declares_readme():
133
+ """真仓库必须声明正文:0.1.1 在 PyPI 上的项目页就是空白的,这就是回归点。"""
134
+ assert [i for i in rc.check_metadata(REPO) if i.level == "error"] == []
135
+ assert rc.readme_target(REPO).endswith(".md")
136
+
137
+
138
+ def test_missing_readme_is_error(tmp_path):
139
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
140
+ assert any(i.code == "no_readme" for i in rc.check_metadata(root))
141
+
142
+
143
+ def test_short_readme_is_error(tmp_path):
144
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
145
+ (root / "pyproject.toml").write_text(
146
+ '[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "README.md"\n',
147
+ encoding="utf-8")
148
+ (root / "README.md").write_text("短\n", encoding="utf-8")
149
+ assert any(i.code == "readme_too_short" for i in rc.check_metadata(root))
150
+
151
+
152
+ def test_readme_pointing_to_missing_file_is_error(tmp_path):
153
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
154
+ (root / "pyproject.toml").write_text(
155
+ '[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "NOPE.md"\n',
156
+ encoding="utf-8")
157
+ assert any(i.code == "readme_missing" for i in rc.check_metadata(root))
158
+
159
+
160
+ # ── 归档内容 ──────────────────────────────────────────────
161
+
162
+ @pytest.mark.parametrize("entry", [
163
+ "foxclaw/.env",
164
+ "foxclaw/.env.local",
165
+ "foxclaw-0.1.0/docs/internal/secret.md",
166
+ "foxclaw/foxclaw_data/sessions.db",
167
+ "foxclaw/old.zip",
168
+ "foxclaw/__pycache__/x.pyc",
169
+ "foxclaw/.venv/lib/x.py",
170
+ ])
171
+ def test_forbidden_entries_detected(entry):
172
+ assert rc.forbidden_entry(entry), entry
173
+
174
+
175
+ @pytest.mark.parametrize("entry", [
176
+ "foxclaw/.env.example", # 模板必须能进包
177
+ "foxclaw/cli.py",
178
+ "foxclaw/vendor/hmr/hmr/__init__.py",
179
+ ])
180
+ def test_allowed_entries_pass(entry):
181
+ assert rc.forbidden_entry(entry) is None
182
+
183
+
184
+ # ── dist / npm ────────────────────────────────────────────
185
+
186
+ def test_check_dist_passes_with_vendor_warning_only(tmp_path):
187
+ dist = tmp_path / "dist"
188
+ dist.mkdir()
189
+ _wheel(dist)
190
+ _sdist(dist)
191
+ issues = rc.check_dist(dist, "foxclaw", "0.1.0")
192
+ assert [i for i in issues if i.level == "error"] == []
193
+ assert any(i.code == "no_vendor" for i in issues), "缺 vendor 应显式提示"
194
+
195
+
196
+ def test_check_dist_flags_missing_wheel(tmp_path):
197
+ dist = tmp_path / "dist"
198
+ dist.mkdir()
199
+ _sdist(dist)
200
+ assert any(i.code == "wheel_count" for i in rc.check_dist(dist, "foxclaw", "0.1.0"))
201
+
202
+
203
+ def test_check_dist_flags_version_drift_in_filename(tmp_path):
204
+ dist = tmp_path / "dist"
205
+ dist.mkdir()
206
+ _wheel(dist, version="0.2.0")
207
+ _sdist(dist, version="0.1.0")
208
+ codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
209
+ assert "wheel_name" in codes or "meta_mismatch" in codes, codes
210
+
211
+
212
+ def test_check_dist_flags_secret_inside_archive(tmp_path):
213
+ dist = tmp_path / "dist"
214
+ dist.mkdir()
215
+ _wheel(dist, extra=(".env",))
216
+ _sdist(dist)
217
+ msgs = [i.msg for i in rc.check_dist(dist, "foxclaw", "0.1.0")
218
+ if i.code == "forbidden_entry"]
219
+ assert msgs and any(".env" in m for m in msgs), msgs
220
+
221
+
222
+ def test_check_dist_flags_empty_long_description(tmp_path):
223
+ dist = tmp_path / "dist"
224
+ dist.mkdir()
225
+ _wheel(dist, description=False)
226
+ _sdist(dist)
227
+ codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
228
+ assert "no_long_description" in codes, codes
229
+
230
+
231
+ def test_check_dist_missing_dir_is_error(tmp_path):
232
+ assert any(i.code == "no_dist"
233
+ for i in rc.check_dist(tmp_path / "nope", "foxclaw", "0.1.0"))
234
+
235
+
236
+ def test_npm_tgz_ok(tmp_path):
237
+ assert rc.check_npm_tgz(_tgz(tmp_path), "foxclaw", "0.1.0") == []
238
+
239
+
240
+ def test_npm_tgz_name_drift_detected(tmp_path):
241
+ tgz = _tgz(tmp_path, pkg={"name": "foxclaw", "version": "0.2.0"})
242
+ codes = {i.code for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0")}
243
+ assert "npm_meta" in codes, codes
244
+
245
+
246
+ def test_npm_tgz_filename_drift_detected(tmp_path):
247
+ tgz = _tgz(tmp_path, version="0.2.0")
248
+ assert any(i.code == "npm_name" for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0"))
249
+
250
+
251
+ def test_run_end_to_end(tmp_path):
252
+ """run() 串起来:真仓库 + 构造的 dist/tgz,无 tag 也应通过。"""
253
+ dist = tmp_path / "dist"
254
+ dist.mkdir()
255
+ issues = rc.run(REPO, tag="", dist=dist, npm_tgz=None)
256
+ assert any(i.level == "error" for i in issues), "空 dist 应报错"
257
+ # 版本号跟着仓库走:写死 0.1.0 会在每次改版本时假红
258
+ version = rc.collect_versions(REPO)["pyproject.toml:project.version"]
259
+ _wheel(dist, version=version)
260
+ _sdist(dist, version=version)
261
+ tgz = _tgz(dist, version=version)
262
+ assert [i for i in rc.run(REPO, f"v{version}", dist, tgz) if i.level == "error"] == []
263
+ shutil.rmtree(dist)