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,129 @@
1
+ """工具中心第1期 · 技能台真实化 + 安装审核门 + 治理 端到端验证。
2
+ 判定:assert 失败→Traceback→run_all 记失败;正常退出=通过。"""
3
+ import sys, os, tempfile
4
+ from pathlib import Path
5
+ sys.path.insert(0, str(Path(__file__).parent.parent))
6
+
7
+ def test_tool_center():
8
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
9
+ _DATA = tempfile.mkdtemp(prefix="foxclaw_toolc_")
10
+ os.environ["FOXCLAW_DATA_DIR"] = _DATA
11
+
12
+ from fastapi.testclient import TestClient
13
+ from foxclaw.api import app
14
+
15
+ WEBAPI = {"id": "wx_weather", "name": "天气", "description": "查天气", "kind": "web_api",
16
+ "triggers": ["天气"], "side_effect": "read",
17
+ "endpoint": "https://example.com/api/weather", "http_method": "GET"}
18
+
19
+ with TestClient(app) as c:
20
+ print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
21
+
22
+ # 1. 安装 web_api 技能(https 只读 → allow)
23
+ r = c.post("/api/ahs/skills", json=WEBAPI)
24
+ assert r.status_code == 200, r.text
25
+ assert r.json()["created"] is True
26
+ print("[1] 安装 web_api 技能 OK")
27
+
28
+ # 2. 列表里出现且 source=user
29
+ r = c.get("/api/ahs/skills"); assert r.status_code == 200
30
+ ids = {s["id"]: s for s in r.json()["skills"]}
31
+ assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
32
+ print("[2] 列表含新技能且 source=user OK")
33
+
34
+ # 3. 拒绝 local kind
35
+ r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
36
+ assert r.status_code == 400
37
+ print("[3] 拒绝 local kind(400) OK")
38
+
39
+ # 4. 拒绝非法 endpoint
40
+ r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
41
+ assert r.status_code == 400
42
+ print("[4] 拒非法 endpoint(400) OK")
43
+
44
+ # 5. mcp 缺字段
45
+ r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
46
+ assert r.status_code == 400
47
+ print("[5] mcp 缺 server/tool(400) OK")
48
+
49
+ # 6. 启停
50
+ r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
51
+ assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
52
+ r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
53
+ assert r.json()["skill"]["enabled"] is True
54
+ print("[6] 启停切换 OK")
55
+
56
+ # 7. 删内置 → 409(取列表里第一个 builtin)
57
+ r = c.get("/api/ahs/skills")
58
+ builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
59
+ if builtin:
60
+ rd = c.delete(f"/api/ahs/skills/{builtin}")
61
+ assert rd.status_code == 409, rd.text
62
+ print("[7] 内置技能不可删(409) OK")
63
+ # 7b. 用内置 id 安装 → 拒(不能覆盖内置)
64
+ rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
65
+ assert rc.status_code == 409, rc.text
66
+ print("[7b] 用内置 id 安装被拒(409) OK")
67
+ else:
68
+ print("[7] 跳过(无内置技能)")
69
+
70
+ # 8. 测试调用:read 技能直接跑(返回 SkillResult 结构)
71
+ r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
72
+ assert r.status_code == 200 and "success" in r.json()
73
+ print("[8] 测试调用返回 SkillResult OK")
74
+
75
+ # 9. 安装审核门:打本地地址的 web_api 安装期被 deny(403)
76
+ r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
77
+ "triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
78
+ assert r.status_code == 403, r.text
79
+ assert r.json()["detail"]["error"] == "install_denied"
80
+ print("[9] 安装审核门 deny 本地地址(403) OK")
81
+
82
+ # 9b. 有副作用技能未确认 → 409 needs_ack;acknowledged 后 → 200(且持久,供步12 round-trip)
83
+ _send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
84
+ "triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
85
+ r = c.post("/api/ahs/skills", json=_send)
86
+ assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True, r.text
87
+ r = c.post("/api/ahs/skills", json={**_send, "acknowledged": True})
88
+ assert r.status_code == 200, r.text
89
+ print("[9b] 副作用技能需确认(409→200) OK")
90
+
91
+ # 10. 治理端点可用
92
+ assert c.get("/api/ahs/breaker").status_code == 200
93
+ assert c.get("/api/ahs/hooks/audit").status_code == 200
94
+ print("[10] 治理端点(breaker/audit) OK")
95
+
96
+ # 10b. 信任分级:内置 calculator(local)=high、内置 mcp=low、用户 web_api=mid
97
+ sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
98
+ assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
99
+ assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"] # 内置但 mcp→低
100
+ assert sk["wx_weather"]["trust"] == "mid"
101
+ print("[10b] 信任分级 high/mid/low OK")
102
+
103
+ # 10c. deny 封杀:封杀后测试调用被拦 + blocked 标;解封恢复
104
+ c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
105
+ r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
106
+ assert r["status"] == "blocked" and "封杀" in r["message"], r
107
+ assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is True
108
+ assert "wx_weather" in c.get("/api/ahs/policy").json()["deny"]
109
+ assert c.delete("/api/ahs/policy/deny/wx_weather").status_code == 200
110
+ assert "wx_weather" not in c.get("/api/ahs/policy").json()["deny"]
111
+ assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
112
+ print("[10c] deny 封杀/解封 kill-switch OK")
113
+
114
+ # 11. 删除用户技能
115
+ assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
116
+ r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
117
+ print("[11] 删除用户技能 OK")
118
+
119
+ # 12. 持久化 round-trip:直建新 OS(同数据目录),已安装的 send_x 应仍在
120
+ from foxclaw import Foxclaw
121
+ os2 = Foxclaw(data_dir=_DATA)
122
+ assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"
123
+ assert os2.skill_registry.get("send_x").source == "user"
124
+ print("[12] 重启后用户技能仍在(持久化 round-trip) OK")
125
+
126
+ print("\n工具中心第1期验证全部通过")
127
+
128
+ if __name__ == "__main__":
129
+ test_tool_center()
@@ -0,0 +1,113 @@
1
+ """工具调度:只读并发 / 写与未声明串行 / 单工具超时 / 污点回主线程合并。
2
+
3
+ 背景:`agents/mixins/tool_exec.py` 原本逐个 `_dispatch_tool` 串行,一个慢工具阻塞整轮,
4
+ 且完全无单工具超时——模型一次发两个只读工具(联网+召回)要等两倍时间,网络卡死则整轮挂住。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ import time
11
+
12
+ from foxclaw.agents.mixins import tool_exec as _te
13
+ from foxclaw.agents.mixins.tool_exec import ToolExecMixin
14
+
15
+
16
+ class _Fake(ToolExecMixin):
17
+ """只保留调度层:闸门走真实 registry + permission,执行被 monkeypatch 替换。"""
18
+
19
+ def __init__(self):
20
+ self.calls: list[str] = []
21
+ self._taint_level = 0
22
+
23
+ def _think(self, **kwargs):
24
+ return None, None
25
+
26
+
27
+ def _make(monkeypatch, behavior) -> _Fake:
28
+ fake = _Fake()
29
+
30
+ def _invoke(agent, name, args, taint_sink=None):
31
+ agent.calls.append(name)
32
+ return behavior(name, args, taint_sink)
33
+
34
+ monkeypatch.setattr(_te, "_invoke_tool", _invoke)
35
+ return fake
36
+
37
+
38
+ def _calls(*pairs):
39
+ return [{"name": n, "args": a} for n, a in pairs]
40
+
41
+
42
+ def test_readonly_tools_run_concurrently(monkeypatch) -> None:
43
+ barrier = threading.Barrier(2, timeout=3)
44
+
45
+ def behave(name, args, sink):
46
+ barrier.wait() # 串行时第二个永远到不了 → BrokenBarrierError
47
+ return f"{name}:ok"
48
+
49
+ fake = _make(monkeypatch, behave)
50
+ t0 = time.monotonic()
51
+ out = fake._run_tools(_calls(("web_search", {"query": "python 3.14"}),
52
+ ("recall_memory", {"query": "项目约定"})))
53
+ elapsed = time.monotonic() - t0
54
+ assert out == ["web_search:ok", "recall_memory:ok"]
55
+ assert elapsed < 2.0, f"两个只读工具应并发执行,实际耗时 {elapsed:.2f}s"
56
+
57
+
58
+ def test_slow_tool_is_cut_off(monkeypatch) -> None:
59
+ monkeypatch.setenv("FOXCLAW_TOOL_TIMEOUT", "0.3")
60
+
61
+ def behave(name, args, sink):
62
+ time.sleep(2)
63
+ return "late"
64
+
65
+ fake = _make(monkeypatch, behave)
66
+ t0 = time.monotonic()
67
+ out = fake._dispatch_tool("web_search", {"query": "x"})
68
+ assert "超过" in out and "未返回" in out
69
+ assert "不要编造" in out
70
+ assert time.monotonic() - t0 < 1.5, "超时后必须立刻返回,不能等工具自己结束"
71
+
72
+
73
+ def test_taint_collected_in_worker_and_merged_in_main(monkeypatch) -> None:
74
+ def behave(name, args, sink):
75
+ sink.append(3 if name == "web_search" else 2) # worker 只能收集,不能直接写会话污点
76
+ return "ok"
77
+
78
+ fake = _make(monkeypatch, behave)
79
+ fake._run_tools(_calls(("web_search", {"query": "x"}),
80
+ ("recall_memory", {"query": "y"})))
81
+ assert fake._taint_level == 3, "并发执行后污点应取最大值合并回会话"
82
+
83
+
84
+ def test_single_readonly_call_still_merged(monkeypatch) -> None:
85
+ def behave(name, args, sink):
86
+ sink.append(2)
87
+ return "ok"
88
+
89
+ fake = _make(monkeypatch, behave)
90
+ assert fake._dispatch_tool("recall_memory", {"query": "y"}) == "ok"
91
+ assert fake._taint_level == 2
92
+
93
+
94
+ def test_undeclared_tool_never_executed_and_batch_serialized(monkeypatch) -> None:
95
+ def behave(name, args, sink):
96
+ return "ok"
97
+
98
+ fake = _make(monkeypatch, behave)
99
+ out = fake._run_tools(_calls(("web_search", {"query": "x"}),
100
+ ("write_file", {"path": "a.md"})))
101
+ assert "未知工具 write_file" in out[1]
102
+ assert fake.calls == ["web_search"], "未声明能力的工具必须被闸门挡住,不进入执行"
103
+
104
+
105
+ def test_missing_required_arg_reports_instead_of_executing(monkeypatch) -> None:
106
+ def behave(name, args, sink): # pragma: no cover - 不应被调用
107
+ raise AssertionError("入参校验失败时不该执行工具")
108
+
109
+ fake = _make(monkeypatch, behave)
110
+ out = fake._run_tools([{"name": "web_search", "args": {}}])
111
+ assert "入参不合法" in out[0]
112
+ assert "query" in out[0]
113
+ assert fake.calls == []
@@ -0,0 +1,103 @@
1
+ """M4 · 工具入参校验 + permission Stage 1 真正生效(回归)。
2
+
3
+ 病根(三条断链):
4
+ 1. tools.py 构造了完整 JSON Schema,但**只发给模型**,执行侧从不校验;
5
+ 2. base._dispatch_tool 构造 permission spec 时传 `params={}`(恒空)
6
+ → permission 管线 Stage 1 的必填校验**永不触发**;
7
+ 3. model_router 解析 tool_calls 参数失败时静默替换为 `{}`
8
+ → 模型给了非法 JSON 也不报错,它以为工具已执行。
9
+
10
+ 本文件锁定:声明的 schema 在执行侧真正生效,且失败对模型可见。
11
+ """
12
+ import sys
13
+ import types
14
+ from pathlib import Path
15
+
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ from foxclaw.core.tools import default_registry, tool_capability
19
+ from foxclaw.skills.permission import evaluate
20
+
21
+
22
+ def check(cond, msg):
23
+ if not cond:
24
+ raise AssertionError(f"✗ {msg}")
25
+ print(f" ✓ {msg}")
26
+
27
+
28
+ def main():
29
+ print("=" * 60)
30
+ print("M4 · 工具入参校验")
31
+ print("=" * 60)
32
+ r = default_registry()
33
+
34
+ print("\n【1】schema 可从注册表取到(执行侧与模型侧同一份真源)")
35
+ sch = r.param_schema("web_search")
36
+ check(bool(sch), "web_search 有 parameters schema")
37
+ check(sch.get("required") == ["query"], f"required 声明正确({sch.get('required')})")
38
+ check(bool(r.param_schema("不存在的工具") == {}), "未知工具返回空 schema(不抛异常)")
39
+
40
+ print("\n【2】必填参数缺失被检出")
41
+ errs = r.validate_args("web_search", {})
42
+ check(any("query" in e for e in errs), f"缺 query 被检出({errs})")
43
+ errs = r.validate_args("web_search", {"query": ""})
44
+ check(any("query" in e for e in errs), f"空字符串 query 也算缺失({errs})")
45
+
46
+ print("\n【3】类型不符被检出(C 类:静默失败改为可见)")
47
+ errs = r.validate_args("web_search", {"query": {"a": 1}})
48
+ check(any("类型" in e for e in errs), f"dict 传 string 位被检出({errs})")
49
+ # top_k 声明在 recall_memory 上(不在 web_search),故用它验 integer 校验
50
+ errs = r.validate_args("recall_memory", {"query": "x", "top_k": "ten"})
51
+ check(any("类型" in e for e in errs), f"字符串传 integer 被检出({errs})")
52
+ errs = r.validate_args("recall_memory", {"query": "x", "top_k": True})
53
+ check(not any("top_k" in e for e in errs),
54
+ f"bool 不被误判为 integer({errs})") # bool 是 int 子类,须排除
55
+ # 未在 schema 中声明的额外字段应放过(不引入 jsonschema 依赖的宽松设计)
56
+ check(r.validate_args("web_search", {"query": "ok", "未声明字段": 1}) == [],
57
+ "未声明的额外字段不报错(避免过度约束模型)")
58
+
59
+ print("\n【4】合法入参通过(不误拦)")
60
+ check(r.validate_args("web_search", {"query": "今天天气"}) == [],
61
+ "合法 query 无错误")
62
+ check(r.validate_args("recall_memory", {"query": "x", "top_k": 8}) == [],
63
+ "合法 top_k 无错误")
64
+
65
+ print("\n【5】permission Stage 1 真正生效(此前 params={} 恒不触发)")
66
+ for tool, args, want in [("web_search", {}, "ask"),
67
+ ("recall_memory", {"query": "x"}, "allow")]:
68
+ sch_t = r.param_schema(tool)
69
+ req = set(sch_t.get("required") or [])
70
+ params = {k: {"required": k in req, "type": v.get("type")}
71
+ for k, v in (sch_t.get("properties") or {}).items()
72
+ if isinstance(v, dict)}
73
+ spec = types.SimpleNamespace(id=f"tool:{tool}", params=params,
74
+ **tool_capability(tool))
75
+ d = evaluate(spec, args, confirmed=False)
76
+ check(d.decision == want,
77
+ f"{tool} args={args} → {d.decision}(期望 {want})")
78
+ if args == {}:
79
+ check("缺少必填参数" in d.reason,
80
+ f"Stage 1 检出缺参:{d.reason}")
81
+
82
+ print("\n【6】参数解析失败不再静默(args_parse_failed 标记存在)")
83
+ import inspect
84
+ from foxclaw.core.model_router import ModelRouter
85
+ # 检查整个类:标记可能随重构从 call_tools 移到 _call_openai_tools 等子方法,
86
+ # 锁「类内存在该标记」而非锁某个方法名(避免重构后误报)。
87
+ src = inspect.getsource(ModelRouter)
88
+ check("args_parse_failed" in src,
89
+ "call_tools 标记解析失败(可观测)")
90
+ check("llm.tool_args_unparsable" in src,
91
+ "解析失败进 tracing(可归因)")
92
+
93
+ print("\n" + "=" * 60)
94
+ print("M4 全部通过")
95
+ print("=" * 60)
96
+
97
+
98
+ def test_tool_params():
99
+ main()
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,242 @@
1
+ """
2
+ P2 function-calling(工具化)守卫测试 —— 全离线,不打真实 LLM/网络。
3
+
4
+ 覆盖:
5
+ 1. P1 运行时事实基准注入(_runtime_context / _inject_runtime 幂等)。
6
+ 2. ToolRegistry:默认工具集、deny 过滤、tool_use 开关读 env。
7
+ 3. ModelRouter.call_tools:不支持 tools 的 provider → supported=False(降级)。
8
+ 4. _reply_with_tools:多轮工具循环(调用→回灌→最终答);不支持 → None(fallback)。
9
+ 5. _dispatch_tool:web_search 命中 / 个人数据否决 / recall_memory / deny 闸。
10
+ """
11
+ import os, sys, types
12
+ from pathlib import Path
13
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
14
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
15
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ from foxclaw.core import tools as T
19
+ from foxclaw.core.model_router import ModelRouter
20
+ from foxclaw.agents.base import CognitiveAgent
21
+
22
+
23
+ # ── 1. P1 运行时注入 ───────────────────────────────────────────────────────────
24
+ def test_runtime_context_and_idempotent():
25
+ r = ModelRouter(provider="stub")
26
+ ctx = r._runtime_context()
27
+ assert "运行时信息" in ctx and "模型" in ctx and "当前日期时间" in ctx
28
+ a = r._inject_runtime("你是助手。")
29
+ assert a.startswith("【运行时信息")
30
+ assert r._inject_runtime(a) == a, "二次注入必须幂等(防 stream→complete 重复)"
31
+
32
+
33
+ # ── 2. ToolRegistry ────────────────────────────────────────────────────────────
34
+ def test_registry_default_and_deny():
35
+ reg = T.default_registry()
36
+ assert set(reg.names()) == {"web_search", "recall_memory"}
37
+ names = [s["function"]["name"] for s in reg.schemas()]
38
+ assert "web_search" in names and "recall_memory" in names
39
+ os.environ["FOXCLAW_TOOL_DENY"] = "web_search"
40
+ try:
41
+ names2 = [s["function"]["name"] for s in reg.schemas()]
42
+ assert "web_search" not in names2 and "recall_memory" in names2
43
+ assert T.is_tool_denied("web_search")
44
+ finally:
45
+ os.environ.pop("FOXCLAW_TOOL_DENY", None)
46
+
47
+
48
+ def test_tool_use_flag_default_off():
49
+ os.environ.pop("FOXCLAW_TOOL_USE", None)
50
+ assert T.tool_use_enabled() is False, "默认必须关(behavior-preserving)"
51
+ os.environ["FOXCLAW_TOOL_USE"] = "1"
52
+ try:
53
+ assert T.tool_use_enabled() is True
54
+ finally:
55
+ os.environ.pop("FOXCLAW_TOOL_USE", None)
56
+
57
+
58
+ # ── 3. call_tools 降级 ─────────────────────────────────────────────────────────
59
+ def test_call_tools_unsupported_degrades():
60
+ r = ModelRouter(provider="stub")
61
+ assert r.supports_tools() is False
62
+ res = r.call_tools("sys", [{"role": "user", "content": "hi"}], [])
63
+ assert res["supported"] is False and res["tool_calls"] == []
64
+
65
+
66
+ # ── 4. 工具循环 ────────────────────────────────────────────────────────────────
67
+ class _FakeRouter:
68
+ """脚本化 call_tools:按序返回预设结果,记录每轮 messages 长度。"""
69
+ def __init__(self, script):
70
+ self.script = list(script)
71
+ self.seen = []
72
+
73
+ def supports_tools(self):
74
+ return True
75
+
76
+ def call_tools(self, system, messages, schemas, tool_choice="auto", agent_id=""):
77
+ self.seen.append((tool_choice, list(messages)))
78
+ return self.script.pop(0)
79
+
80
+ @staticmethod
81
+ def format_tool_result(tc, result):
82
+ return {"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)}
83
+
84
+
85
+ def _fake_agent(router):
86
+ a = types.SimpleNamespace()
87
+ a.router = router
88
+ a.system_prompt = "sys"
89
+ a.AGENT_ID = "assistant"
90
+ a._dispatch_tool = lambda name, args: f"RESULT[{name}:{args.get('query','')}]"
91
+ return a
92
+
93
+
94
+ def test_reply_with_tools_multi_round():
95
+ router = _FakeRouter([
96
+ # 第1轮:模型要求调 web_search
97
+ {"supported": True, "text": "", "assistant_msg": {"role": "assistant", "content": ""},
98
+ "tool_calls": [{"id": "c1", "name": "web_search", "args": {"query": "最新模型"}}]},
99
+ # 第2轮:拿到结果后给最终答案
100
+ {"supported": True, "text": "根据搜索,最新是 X。", "tool_calls": [],
101
+ "assistant_msg": {"role": "assistant", "content": "根据搜索,最新是 X。"}},
102
+ ])
103
+ agent = _fake_agent(router)
104
+ out = CognitiveAgent._reply_with_tools(agent, "有哪些新模型",
105
+ [{"role": "user", "content": "有哪些新模型"}])
106
+ assert out == "根据搜索,最新是 X。"
107
+ # 第2轮的 messages 必须已追加 assistant(带 tool_calls)+ tool 结果
108
+ _, round2_msgs = router.seen[1]
109
+ roles = [m["role"] for m in round2_msgs]
110
+ assert "assistant" in roles and "tool" in roles
111
+ assert any("RESULT[web_search" in str(m.get("content", "")) for m in round2_msgs)
112
+
113
+
114
+ def test_reply_with_tools_direct_answer_no_tool():
115
+ router = _FakeRouter([
116
+ {"supported": True, "text": "你好呀!", "tool_calls": [],
117
+ "assistant_msg": {"role": "assistant", "content": "你好呀!"}},
118
+ ])
119
+ agent = _fake_agent(router)
120
+ out = CognitiveAgent._reply_with_tools(agent, "你好", [{"role": "user", "content": "你好"}])
121
+ assert out == "你好呀!" and len(router.seen) == 1
122
+
123
+
124
+ def test_reply_with_tools_unsupported_returns_none():
125
+ agent = types.SimpleNamespace(router=types.SimpleNamespace(supports_tools=lambda: False),
126
+ system_prompt="s", AGENT_ID="a")
127
+ out = CognitiveAgent._reply_with_tools(agent, "t", [{"role": "user", "content": "t"}])
128
+ assert out is None, "不支持 tools 必须返回 None 让上层 fallback"
129
+
130
+
131
+ def test_reply_with_tools_empty_answer_returns_none():
132
+ # 阶段五·步骤15:模型无工具调用且文本为空 → 返回 None 让上层 fallback(不再"空回复且不回退")
133
+ router = _FakeRouter([
134
+ {"supported": True, "text": "", "tool_calls": [],
135
+ "assistant_msg": {"role": "assistant", "content": ""}},
136
+ ])
137
+ agent = _fake_agent(router)
138
+ out = CognitiveAgent._reply_with_tools(agent, "你好", [{"role": "user", "content": "你好"}])
139
+ assert out is None, "空答必须返回 None 让上层 fallback"
140
+
141
+
142
+ # ── 5. 工具分派 ────────────────────────────────────────────────────────────────
143
+ def test_dispatch_web_search_ok(monkeypatch):
144
+ from foxclaw.core import web_search as ws
145
+ monkeypatch.setattr(ws, "search_answer", lambda q: {"answer": "实时答案XYZ", "references": []})
146
+ agent = types.SimpleNamespace(_think=lambda **k: None)
147
+ out = CognitiveAgent._dispatch_tool(agent, "web_search", {"query": "OpenAI 新模型"})
148
+ assert "实时答案XYZ" in out
149
+
150
+
151
+ def test_dispatch_web_search_personal_veto():
152
+ agent = types.SimpleNamespace(_think=lambda **k: None)
153
+ out = CognitiveAgent._dispatch_tool(agent, "web_search", {"query": "我本月支出多少"})
154
+ assert "本地" in out, "私人财务问题不得外发联网"
155
+
156
+
157
+ def test_dispatch_recall_memory():
158
+ agent = types.SimpleNamespace(
159
+ _recall_memory=lambda q, top_k=5: [{"content": "用户喜欢灰色"}])
160
+ out = CognitiveAgent._dispatch_tool(agent, "recall_memory", {"query": "颜色偏好"})
161
+ assert "灰色" in out
162
+
163
+
164
+ def test_dispatch_denied(monkeypatch):
165
+ monkeypatch.setenv("FOXCLAW_TOOL_DENY", "web_search")
166
+ agent = types.SimpleNamespace(_think=lambda **k: None)
167
+ out = CognitiveAgent._dispatch_tool(agent, "web_search", {"query": "x"})
168
+ assert "禁用" in out
169
+
170
+
171
+ # ── 6. 动作级权限闸(permission 管线 + HITL)──────────────────────────────────────
172
+ def test_tool_capability_readonly_and_failclosed():
173
+ assert T.tool_capability("web_search")["read_only"] is True
174
+ assert T.tool_capability("recall_memory")["side_effect"] == "read"
175
+ # 未声明的新工具 → fail-closed:当有副作用 + 外部
176
+ cap = T.tool_capability("some_future_write_tool")
177
+ assert cap["read_only"] is False and cap["side_effect"] == "write" and cap["external"] is True
178
+
179
+
180
+ def test_permission_gate_allows_readonly_asks_sideeffect():
181
+ import types as _t
182
+ from foxclaw.skills import permission as perm
183
+ ro = _t.SimpleNamespace(id="tool:web_search", params={}, **T.tool_capability("web_search"))
184
+ assert perm.evaluate(ro, {}, confirmed=False).decision == "allow"
185
+ send = _t.SimpleNamespace(id="tool:send_email", params={},
186
+ side_effect="send", read_only=False, external=True, destructive=False)
187
+ d = perm.evaluate(send, {}, confirmed=False)
188
+ assert d.decision == "ask" and d.requires_confirmation
189
+
190
+
191
+ def test_dispatch_hitl_blocks_side_effect_tool(monkeypatch):
192
+ """核心:若工具被判为有副作用(写/发送),_dispatch_tool 走 HITL——不执行、返回需确认。"""
193
+ from foxclaw.core import web_search as ws
194
+ called = {"n": 0}
195
+ monkeypatch.setattr(ws, "search_answer",
196
+ lambda q: called.__setitem__("n", called["n"] + 1) or {"answer": "x"})
197
+ # 把 web_search 强制降级为有副作用 → 应被闸门拦成 HITL
198
+ monkeypatch.setattr(T, "tool_capability",
199
+ lambda name: {"side_effect": "send", "read_only": False,
200
+ "external": True, "destructive": False})
201
+ agent = types.SimpleNamespace(_think=lambda **k: None)
202
+ out = CognitiveAgent._dispatch_tool(agent, "web_search", {"query": "x"})
203
+ assert "确认" in out and called["n"] == 0, "有副作用工具必须 HITL、不自动执行"
204
+
205
+
206
+ def test_dispatch_unknown_tool():
207
+ agent = types.SimpleNamespace(_think=lambda **k: None)
208
+ out = CognitiveAgent._dispatch_tool(agent, "nonexistent_tool", {"x": 1})
209
+ assert "未知工具" in out
210
+
211
+
212
+ def test_tool_path_injects_preferences(monkeypatch):
213
+ """回归(Finding 1):工具路径也要注入用户偏好,不能开工具化就丢掉"越用越懂你"。"""
214
+ monkeypatch.setenv("FOXCLAW_TOOL_USE", "1")
215
+ captured = {}
216
+ fake = types.SimpleNamespace()
217
+ fake._sanitize_history = lambda h, max_turns=12: []
218
+ fake.router = types.SimpleNamespace(supports_tools=lambda: True)
219
+ fake.AGENT_ID = "assistant"
220
+ fake._ability_augment = lambda t, m: m
221
+ fake._skill_augment = lambda t, m: m
222
+ # 偏好注入:把标记并进最后一条 user
223
+ fake._pref_augment = lambda m: (list(m[:-1]) +
224
+ [{"role": "user", "content": "【用户偏好】喜欢简洁\n\n" + m[-1]["content"]}])
225
+
226
+ def _rwt(task, messages, stop_event=None):
227
+ captured["msgs"] = messages
228
+ return "好的,已处理。"
229
+ fake._reply_with_tools = _rwt
230
+ fake._think = lambda **k: None
231
+ fake._store_memory = lambda *a, **k: None
232
+ fake._maybe_store_preference = lambda t: None
233
+
234
+ out = "".join(CognitiveAgent.stream_reply(fake, "帮我总结这段"))
235
+ assert "已处理" in out
236
+ assert any("喜欢简洁" in str(x.get("content", "")) for x in captured["msgs"]), \
237
+ "工具路径必须注入用户偏好(Finding 1)"
238
+
239
+
240
+ if __name__ == "__main__":
241
+ import pytest
242
+ pytest.main([__file__, "-v"])