foxclaw 0.1.2 → 0.1.3

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 (344) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -52,11 +52,9 @@ def test_process_manager():
52
52
  check(pm.get(p.process_id).state == "completed", "完整生命周期到 completed")
53
53
  check(pm.get(p.process_id).state_history == ["created", "queued", "scheduled", "running", "completed"],
54
54
  "状态历史完整")
55
- # 幂等:已终态再标记不崩溃,且**不重复扣减**活跃计数
56
55
  r = pm.mark_failed(p.process_id, "二次")
57
56
  check(r is not None and r.state == "completed", "终态重复标记幂等不崩溃")
58
57
  check(pm.active_count() == 0, "终态重复标记不重复扣减活跃计数(仍为0,不为负)")
59
- # 非法流转拦截
60
58
  p2 = pm.create("x", schedule={"priority": 0.1})
61
59
  p2.state = "created"
62
60
  try:
@@ -89,7 +87,6 @@ def test_truth_pipeline():
89
87
  bus = SystemBus()
90
88
  hmr = FakeHMR()
91
89
  tp = TruthPipeline(bus, hmr=hmr)
92
- # 【P3-9】TruthPipeline.emit_execution 已删除:事件名即契约,直接经 bus 入队。
93
90
  bus.publish("execution.completed", {"agent_id": "finance", "task": "分析预算",
94
91
  "result": "预算正常", "blocked": False})
95
92
  check(tp.last_trace is not None, "执行→审计 trace 生成")
@@ -110,11 +107,9 @@ def test_cognitive_state():
110
107
  s = t.snapshot()
111
108
  check(s["active_goal"] == "分析财务", "活跃目标聚合")
112
109
  check(s["semantic_focus"] == "reasoning", "语义焦点聚合")
113
- # 去重
114
110
  for _ in range(5):
115
111
  bus.publish("memory.updated", {"memory_update": {"content": "同样"}})
116
112
  check(len(t.snapshot()["activated_memories"]) == 1, "重复记忆去重")
117
- # None 防护
118
113
  class M: payload = None
119
114
  t._on_exec(M())
120
115
  check(True, "None payload 不崩溃")
@@ -147,8 +142,7 @@ def test_trust_single_source():
147
142
  from foxclaw.skills import permission as perm
148
143
  from foxclaw.skills.permission import evaluate
149
144
  check(not hasattr(perm, "TRUST_LEVEL"), "permission.TRUST_LEVEL 死字典已删(不再双真源)")
150
- # 3 个合法 kind × 两种 source:permission 的 downgrade 恒等于 (trust_level()=='low'),且行为不变
151
- expect = {"local": False, "web_api": False, "mcp": True} # behavior-preserving 基线
145
+ expect = {"local": False, "web_api": False, "mcp": True}
152
146
  for kind, want_dg in expect.items():
153
147
  for source in ("builtin", "user"):
154
148
  s = SkillSpec(id=f"s_{kind}_{source}", name="t", description="d",
@@ -174,7 +168,6 @@ def test_breaker_hooks():
174
168
  check(ad.breaker.status("flaky")["state"] == "open", "连续失败3次断路器开闸")
175
169
  r = ad.invoke(reg.get("flaky"), {})
176
170
  check(r.status == "circuit_open", "开闸后停止调用")
177
- # 钩子
178
171
  log = []
179
172
  hm = HookManager()
180
173
  hm.register("pre_skill", lambda ctx: log.append("pre"))
@@ -193,13 +186,10 @@ def test_ahs_pipeline():
193
186
  reg = build_default_registry()
194
187
  ad = SkillAdapter(reg)
195
188
  pipe = AHSPipeline(reg, ad)
196
- # router 否定词
197
189
  r = route("不要计算", reg)
198
190
  check(r.primary != "calculator", "否定词检测:不要计算→不选calculator")
199
- # planner 拆解
200
191
  p = plan("先算 5+5 然后再算 9*9", reg)
201
192
  check(len(p.tasks) == 2, "planner 多步拆解")
202
- # 端到端
203
193
  res = pipe.run("帮我算一下 12*12")
204
194
  check(res.success, "AHS 执行链端到端成功")
205
195
 
@@ -27,23 +27,19 @@ def main():
27
27
  print(f"\n采集 {n} 个知识点")
28
28
  check(n == 8, f"采集 8 个知识点(实为 {n})")
29
29
 
30
- # 1. 间隔重复:模拟几次复习
31
30
  print("\n【固化:间隔重复】")
32
31
  ids = list(rt.items.keys())
33
32
  check(len(ids) >= 4, f"至少 4 个知识点可供复习(实为 {len(ids)})")
34
- # 模拟:前两个记得好,第三个记不住
35
- rt.review(ids[0], grade=5) # 完美
33
+ rt.review(ids[0], grade=5)
36
34
  rt.review(ids[0], grade=4)
37
35
  rt.review(ids[1], grade=4)
38
- rt.review(ids[2], grade=1) # 没记住
39
- rt.review(ids[2], grade=2) # 又没记住
36
+ rt.review(ids[2], grade=1)
37
+ rt.review(ids[2], grade=2)
40
38
  for iid in ids[:4]:
41
39
  st = rt.review_states[iid]
42
40
  item = rt.items[iid]
43
41
  print(f" 掌握度 {st.mastery:.2f} | 间隔 {st.interval}天 | "
44
42
  f"复习{st.total_reviews}次 遗忘{st.lapses}次 | {item.content[:24]}...")
45
- # SM-2 的语义后果:连续答不好应累积 lapses,且掌握度被压低。
46
- # 这是"间隔重复真的在起作用"的核心断言 —— 只打印 0.60/0.11 两个数字不算验证。
47
43
  st_good, st_bad = rt.review_states[ids[0]], rt.review_states[ids[2]]
48
44
  check(st_bad.lapses == 2, f"连错两次的知识点累积 lapses=2(实为 {st_bad.lapses})")
49
45
  check(st_bad.mastery < st_good.mastery,
@@ -51,7 +47,6 @@ def main():
51
47
  check(st_good.total_reviews == 2 and st_bad.total_reviews == 2,
52
48
  "复习次数被正确累计(各 2 次)")
53
49
 
54
- # 2. 知识图谱:构建连接
55
50
  print("\n【连接:知识图谱】")
56
51
  conn = rt.build_connections(semantic_threshold=0.5)
57
52
  print(f" 同主题边: {conn['topic_edges']} · 语义边: {conn['semantic_edges']}")
@@ -61,12 +56,10 @@ def main():
61
56
  print(f" 边类型: {g['edge_types']}")
62
57
  check(conn["topic_edges"] > 0, f"同主题连接已建立({conn['topic_edges']} 条)")
63
58
  check(g["items"] == n, f"图谱节点数 = 知识点数({g['items']} == {n})")
64
- # 结构不变式:总边数必须等于各类边之和,否则说明有一类边被漏计/重复计
65
59
  check(g["edges"] == conn["topic_edges"] + conn["semantic_edges"],
66
60
  f"总边数 = 同主题边 + 语义边({g['edges']} == {conn['topic_edges']}+{conn['semantic_edges']})")
67
61
  check(g["clusters"] > 0, f"聚类非空({g['clusters']} 个知识簇)")
68
62
 
69
- # 3. 连接发现:某知识点的关联
70
63
  print("\n【连接发现】")
71
64
  related = rt.related(ids[0])
72
65
  base = rt.items[ids[0]].content[:20]
@@ -77,7 +70,6 @@ def main():
77
70
  check(all(("edge_type" in x and "weight" in x and "item" in x) for x in related),
78
71
  "每条关联都带 edge_type / weight / item 结构")
79
72
 
80
- # 4. 综合分析
81
73
  print("\n【综合分析】")
82
74
  rep = rt.analyze()
83
75
  print(f" 知识点 {rep['total_items']} · 平均掌握度 {rep['avg_mastery']}")
@@ -89,7 +81,6 @@ def main():
89
81
  check(rep["weakest_topic"] in rep["topic_mastery"],
90
82
  f"最薄弱主题来自主题掌握度表(weakest_topic={rep['weakest_topic']})")
91
83
 
92
- # 5. 学习策略裁决
93
84
  print("\n【学习策略裁决】")
94
85
  for v in rep["strategy"]:
95
86
  print(f" [{v['action']}] {v['message']}")
@@ -11,7 +11,6 @@ import os, sys, json, tempfile
11
11
  from pathlib import Path
12
12
  sys.path.insert(0, str(Path(__file__).parent.parent))
13
13
 
14
- # ── 环境隔离:必须在 import foxclaw 之前 ──────────────────────────────
15
14
  for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "GOOGLE_API_KEY",
16
15
  "DASHSCOPE_API_KEY", "MINIMAX_API_KEY", "LLM_API_KEY", "FOXCLAW_API_KEY"):
17
16
  os.environ.pop(_k, None)
@@ -32,17 +31,14 @@ import foxclaw.api as _api
32
31
  from foxclaw.api.routes import llm_admin
33
32
 
34
33
 
35
- # ── 1. BASE URL 规范化 ───────────────────────────────────────────────
36
-
37
34
  @pytest.mark.parametrize("raw,expected", [
38
35
  ("https://api.deepseek.com", "https://api.deepseek.com/v1"),
39
36
  ("https://api.deepseek.com/", "https://api.deepseek.com/v1"),
40
37
  ("https://api.deepseek.com/v1", "https://api.deepseek.com/v1"),
41
38
  ("https://api.deepseek.com/v1/", "https://api.deepseek.com/v1"),
42
39
  (" https://api.openai.com/v1 ", "https://api.openai.com/v1"),
43
- ("api.deepseek.com", "https://api.deepseek.com/v1"), # 漏 scheme
40
+ ("api.deepseek.com", "https://api.deepseek.com/v1"),
44
41
  ("api.deepseek.com/v1", "https://api.deepseek.com/v1"),
45
- # 已有非 /v1 的 path 必须原样保留(通义兼容模式 / 自建网关 /openai/v1)
46
42
  ("https://dashscope.aliyuncs.com/compatible-mode/v1",
47
43
  "https://dashscope.aliyuncs.com/compatible-mode/v1"),
48
44
  ("http://localhost:11434/v1", "http://localhost:11434/v1"),
@@ -50,7 +46,6 @@ from foxclaw.api.routes import llm_admin
50
46
  ])
51
47
  def test_normalize_base_url(raw, expected):
52
48
  assert normalize_base_url(raw) == expected
53
- # 幂等:规范化两次结果不变(前端 blur + 后端保存 + router 构造都会过一遍)
54
49
  assert normalize_base_url(expected) == expected
55
50
 
56
51
 
@@ -60,8 +55,6 @@ def test_normalize_base_url_rejects(bad):
60
55
  normalize_base_url(bad)
61
56
 
62
57
 
63
- # ── 2. custom 必须被当作 openai-compatible 家族成员 ──────────────────
64
-
65
58
  def test_custom_is_openai_family():
66
59
  assert "custom" in ModelRouter._OPENAI_FAMILY, \
67
60
  "custom 漏了会让流式退化为一次性输出、工具能力静默关闭"
@@ -71,28 +64,22 @@ def test_custom_is_openai_family():
71
64
  def test_custom_provider_supports_tools():
72
65
  r = ModelRouter(provider="stub")
73
66
  r.provider = "custom"
74
- r._client = object() # 假装已构造好(不联网)
67
+ r._client = object()
75
68
  assert r.supports_tools() is True
76
69
 
77
70
 
78
- # ── 3. stream() 必须 reload 选择(聊天主路径)─────────────────────────
79
-
80
71
  def test_stream_reloads_selection():
81
72
  r = ModelRouter(provider="stub")
82
73
  hits = []
83
- r._maybe_reload_selection = lambda: hits.append(True) # 不打真实文件
74
+ r._maybe_reload_selection = lambda: hits.append(True)
84
75
  list(r.stream("sys", [{"role": "user", "content": "hi"}]))
85
76
  assert hits, "stream() 未调用 _maybe_reload_selection → 切模型后流式对话不生效"
86
77
 
87
78
 
88
- # ── 4. 保存 / 切换端点 ───────────────────────────────────────────────
89
-
90
79
  @pytest.fixture()
91
80
  def fresh_store(monkeypatch):
92
81
  """每个用例一份干净的 custom_models.json + .current_model。"""
93
82
  d = Path(tempfile.mkdtemp(prefix="llm_admin_store_"))
94
- # router 侧按 env 现算路径(_custom_models_path / _selection_path),必须一起指过来,
95
- # 否则端点写 A 目录、router 读 B 目录 → 预检假失败。
96
83
  monkeypatch.setenv("FOXCLAW_DATA_DIR", str(d))
97
84
  monkeypatch.setattr(llm_admin, "CUSTOM_MODELS_PATH", d / "custom_models.json")
98
85
  monkeypatch.setattr(llm_admin, "CURRENT_MODEL_PATH", d / ".current_model")
@@ -110,7 +97,6 @@ def test_add_normalizes_base_url(fresh_store):
110
97
  saved = json.loads((fresh_store / "custom_models.json").read_text(encoding="utf-8"))
111
98
  assert saved[0]["base_url"] == "https://api.deepseek.com/v1"
112
99
 
113
- # 更新时同样规范化
114
100
  import asyncio
115
101
  asyncio.run(llm_admin.update_custom_model("deepseek", llm_admin._CustomUpdateReq(
116
102
  base_url="https://api.deepseek.com")))
@@ -133,7 +119,7 @@ def test_select_custom_takes_effect_immediately(fresh_store, monkeypatch):
133
119
  pass
134
120
  fake = _FakeOS()
135
121
  fake.router = router
136
- monkeypatch.setattr(_api, "_os_instance", fake, raising=False)
122
+ monkeypatch.setattr(_api._state, "_os_instance", fake, raising=False)
137
123
 
138
124
  import asyncio
139
125
  out = asyncio.run(llm_admin.select_model(
@@ -143,7 +129,6 @@ def test_select_custom_takes_effect_immediately(fresh_store, monkeypatch):
143
129
  assert out["runtime"]["provider"] == "custom"
144
130
  assert out["runtime"]["model"] == "deepseek-chat"
145
131
  assert router.provider == "custom" and router.model == "deepseek-chat"
146
- # .current_model 落盘,重启后 lazy reload 仍能读到
147
132
  cur = json.loads((fresh_store / ".current_model").read_text(encoding="utf-8"))
148
133
  assert cur["source"] == "custom" and cur["name"] == "deepseek"
149
134
 
@@ -83,20 +83,15 @@ def test_build_plan_tool_schema():
83
83
 
84
84
  def test_llm_plan_failsafe():
85
85
  print("\n【3】fail-safe:不支持/不产出/解析失败/空输入/抛异常 → None")
86
- # 不支持 tools
87
86
  r = _FakeRouter(result={"supported": False})
88
87
  assert llm_plan(r, "任务", _catalog()) is None
89
- # 模型没调用工具(空 tool_calls)
90
88
  r2 = _FakeRouter(result={"supported": True, "tool_calls": []})
91
89
  assert llm_plan(r2, "任务", _catalog()) is None
92
- # args 解析失败
93
90
  r3 = _FakeRouter(result={"supported": True, "tool_calls": [
94
91
  {"id": "c1", "name": "submit_plan", "args": {}, "args_parse_failed": True}]})
95
92
  assert llm_plan(r3, "任务", _catalog()) is None
96
- # 空 catalog / 空 task
97
93
  assert llm_plan(r2, "任务", {}) is None
98
94
  assert llm_plan(r2, "", _catalog()) is None
99
- # call_tools 抛异常 → None(不冒泡)
100
95
 
101
96
  class _Boom:
102
97
  def call_tools(self, *a, **k):
@@ -120,13 +115,11 @@ def test_llm_plan_parse():
120
115
  assert [s["agent"] for s in out["steps"]] == ["finance", "finance", "office"]
121
116
  assert out["steps"][1]["depends_on"] == [0]
122
117
 
123
- # 单步 → flow 强制 single
124
118
  r2 = _FakeRouter(result={"supported": True, "tool_calls": [
125
119
  _plan_args("parallel", [{"name": "查天气", "agent": "office"}])]})
126
120
  out2 = llm_plan(r2, "查天气", _catalog())
127
121
  assert out2["flow"] == "single" and len(out2["steps"]) == 1
128
122
 
129
- # 非法 agent(含已下放的 assistant/admin)兜底 office
130
123
  r3 = _FakeRouter(result={"supported": True, "tool_calls": [
131
124
  _plan_args("single", [{"name": "x", "agent": "不存在的Agent"}])]})
132
125
  out3 = llm_plan(r3, "x", _catalog())
@@ -140,14 +133,12 @@ def test_supervisor_llm_plan():
140
133
  D = type("D", (), {"AGENT_NAME": "dummy"})
141
134
  sub = {"finance": D(), "office": D()}
142
135
 
143
- # 未开启 → None
144
136
  os.environ.pop("FOXCLAW_LLM_PLAN", None)
145
137
  a1 = SupervisorAgent.__new__(SupervisorAgent)
146
138
  a1.router = _FakeRouter(result={"supported": True})
147
139
  a1._subagents = sub
148
140
  assert a1._llm_plan("任务") is None
149
141
 
150
- # 开启 + 多步 → (Plan, step_routes)
151
142
  os.environ["FOXCLAW_LLM_PLAN"] = "1"
152
143
  a2 = SupervisorAgent.__new__(SupervisorAgent)
153
144
  a2.router = _FakeRouter(result={"supported": True, "tool_calls": [
@@ -161,14 +152,12 @@ def test_supervisor_llm_plan():
161
152
  assert len(plan.tasks) == 2
162
153
  assert routes == [("查支出", "finance"), ("发邮件", "office")]
163
154
 
164
- # 单步 → None(交回关键词路由,不改变单步行为)
165
155
  a3 = SupervisorAgent.__new__(SupervisorAgent)
166
156
  a3.router = _FakeRouter(result={"supported": True, "tool_calls": [
167
157
  _plan_args("single", [{"name": "查天气", "agent": "office"}])]})
168
158
  a3._subagents = sub
169
159
  assert a3._llm_plan("查天气") is None
170
160
 
171
- # 不支持 tools → None
172
161
  a4 = SupervisorAgent.__new__(SupervisorAgent)
173
162
  a4.router = _FakeRouter(is_stub=True)
174
163
  a4._subagents = sub
@@ -192,7 +181,6 @@ def test_llm_replan_parse():
192
181
  print("\n【7】llm_replan:修正计划 + 空 steps 放弃 + fail-safe")
193
182
  prior = [{"subtask": "查支出", "agent": "finance", "output": "超时", "blocked": True},
194
183
  {"subtask": "发邮件", "agent": "office", "output": "未执行", "blocked": False}]
195
- # 正常:换 agent 重排失败步骤
196
184
  r = _FakeRouter(result={"supported": True, "tool_calls": [
197
185
  _plan_args("sequential", [
198
186
  {"name": "查支出(重试)", "agent": "finance"},
@@ -200,12 +188,10 @@ def test_llm_replan_parse():
200
188
  ])]})
201
189
  out = llm_replan(r, "查支出再发邮件", prior, _catalog())
202
190
  assert out is not None and [s["agent"] for s in out["steps"]] == ["finance", "office"]
203
- # 空 steps = 模型判定放弃 → 返回 {"steps": []} 而非 None
204
191
  r2 = _FakeRouter(result={"supported": True, "tool_calls": [
205
192
  _plan_args("sequential", [])]})
206
193
  out2 = llm_replan(r2, "任务", prior, _catalog())
207
194
  assert out2 is not None and out2["steps"] == []
208
- # fail-safe:不支持 tools → None
209
195
  r3 = _FakeRouter(result={"supported": False})
210
196
  assert llm_replan(r3, "任务", prior, _catalog()) is None
211
197
  print(" ✓ 修正计划 / 空steps放弃 / 不支持回退 正确")
@@ -214,12 +200,9 @@ def test_llm_replan_parse():
214
200
  def test_dispatch_failed():
215
201
  print("\n【8】_dispatch_failed:失败判定")
216
202
  from foxclaw.agents.agents import _dispatch_failed
217
- # 多步 blocked → True
218
203
  assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": True}}) is True
219
- # parallel steps 含 failed → True
220
204
  assert _dispatch_failed({"result": {"dispatched_to": "parallel",
221
205
  "parallel_steps": [{"failed": True}]}}) is True
222
- # 单 Agent / 成功 / 非 dict → False
223
206
  assert _dispatch_failed({"result": {"dispatched_to": "finance"}}) is False
224
207
  assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": False,
225
208
  "multi_steps": [{"blocked": False}]}}) is False
@@ -235,7 +218,6 @@ def test_replan_and_redo():
235
218
  D = type("D", (), {"AGENT_NAME": "dummy"})
236
219
  sub = {"finance": D(), "office": D()}
237
220
 
238
- # 反思时返回修正计划(重试失败步骤)
239
221
  router = _FakeRouter(result={"supported": True, "tool_calls": [
240
222
  _plan_args("sequential", [
241
223
  {"name": "查支出(重试)", "agent": "finance"},
@@ -244,7 +226,7 @@ def test_replan_and_redo():
244
226
  a = SupervisorAgent.__new__(SupervisorAgent)
245
227
  a.router = router
246
228
  a._subagents = sub
247
- a._think = lambda **kw: None # 屏蔽 thought_chain
229
+ a._think = lambda **kw: None
248
230
 
249
231
  calls = {"n": 0}
250
232
 
@@ -283,7 +265,6 @@ def test_replan_gives_up():
283
265
  D = type("D", (), {"AGENT_NAME": "dummy"})
284
266
  sub = {"finance": D(), "assistant": D()}
285
267
 
286
- # 反思返回空 steps(放弃)
287
268
  router = _FakeRouter(result={"supported": True, "tool_calls": [
288
269
  _plan_args("sequential", [])]})
289
270
  a = SupervisorAgent.__new__(SupervisorAgent)
@@ -314,21 +295,18 @@ def test_sv_replan_stage():
314
295
  from types import SimpleNamespace
315
296
  from foxclaw.pipeline.supervisor_stages import sv_replan
316
297
 
317
- # 无失败 → 短路
318
298
  a1 = SimpleNamespace(_replan_enabled=lambda: True)
319
299
  ctx1 = SimpleNamespace(extra={}, agent=a1,
320
300
  dispatch_result={"result": {"dispatched_to": "multi", "blocked": False}})
321
301
  asyncio.run(sv_replan(ctx1))
322
302
  assert ctx1.dispatch_result["result"]["blocked"] is False
323
303
 
324
- # 有失败但未开启 → 短路保留原结果
325
304
  a2 = SimpleNamespace(_replan_enabled=lambda: False)
326
305
  ctx2 = SimpleNamespace(extra={}, agent=a2,
327
306
  dispatch_result={"result": {"dispatched_to": "multi", "blocked": True}})
328
307
  asyncio.run(sv_replan(ctx2))
329
308
  assert ctx2.dispatch_result["result"]["blocked"] is True
330
309
 
331
- # 触发路径:有失败 + 开启 → 调用 _replan_and_redo 更新结果
332
310
  async def fake_replan(task, sched, session_id, subagents, context, memories, dr, max_rounds=2):
333
311
  return {"result": {"dispatched_to": "multi", "blocked": False}}
334
312
 
@@ -16,58 +16,47 @@ def test_markdown_skills():
16
16
  with TestClient(app) as c:
17
17
  print("=" * 60); print("工具中心 · SKILL.md 指令技能 验证"); print("=" * 60)
18
18
 
19
- # 1. 缺 ack → 409
20
19
  r = c.post("/api/ahs/md-skills", json={"content": SKILL})
21
20
  assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
22
21
  print("[1] 未确认 → 409 needs_ack OK")
23
22
 
24
- # 2. ack → 200,列表出现
25
23
  r = c.post("/api/ahs/md-skills", json={"content": SKILL, "acknowledged": True})
26
24
  assert r.status_code == 200, r.text
27
25
  sid = r.json()["skill"]["id"]
28
26
  metas = c.get("/api/ahs/md-skills").json()["skills"]
29
27
  assert any(s["id"] == sid for s in metas)
30
- assert all("body" not in s for s in metas) # 列表不含 body
28
+ assert all("body" not in s for s in metas)
31
29
  print("[2] 上传(ack)+列表(不含body) OK")
32
30
 
33
- # 3. GET/{id} 含 body
34
31
  r = c.get(f"/api/ahs/md-skills/{sid}")
35
32
  assert r.status_code == 200 and "成果" in r.json()["body"]
36
33
  print("[3] 查看含 body OK")
37
34
 
38
- # 4. 缺 name → 400
39
35
  r = c.post("/api/ahs/md-skills", json={"content": "---\ndescription: x\n---\nbody", "acknowledged": True})
40
36
  assert r.status_code == 400
41
37
  print("[4] 缺 name → 400 OK")
42
38
 
43
- # 5. 启停
44
39
  assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": False}).json()["skill"]["enabled"] is False
45
40
  assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": True}).status_code == 200
46
41
  print("[5] 启停 OK")
47
42
 
48
- # 6. 注入:直建 OS,agent 挂了 md_skills,_skill_augment 命中注入 + 持久化
49
43
  from foxclaw import Foxclaw
50
44
  os2 = Foxclaw(data_dir=_DATA)
51
45
  assert os2.md_skills.get(sid) is not None, "重启后指令技能丢失(持久化)"
52
46
  ag = os2.supervisor
53
47
  out = ag._skill_augment("帮我写周报", [{"role": "user", "content": "帮我写周报"}])
54
48
  assert "先列本周成果" in out[-1]["content"] and "帮我写周报" in out[-1]["content"]
55
- assert sum(1 for i in range(len(out) - 1) if out[i]["role"] == out[i + 1]["role"] == "user") == 0 # 无连续 user
49
+ assert sum(1 for i in range(len(out) - 1) if out[i]["role"] == out[i + 1]["role"] == "user") == 0
56
50
  out2 = ag._skill_augment("订机票", [{"role": "user", "content": "订机票"}])
57
- assert out2[0]["content"] == "订机票" # 不含触发词 → 原样
51
+ assert out2[0]["content"] == "订机票"
58
52
  print("[6] 注入命中/未命中 + 持久化 round-trip OK")
59
53
 
60
- # 7. 删除
61
54
  with TestClient(app) as c:
62
55
  assert c.delete(f"/api/ahs/md-skills/{sid}").status_code == 200
63
56
  assert not any(s["id"] == sid for s in c.get("/api/ahs/md-skills").json()["skills"])
64
57
  print("[7] 删除 OK")
65
58
 
66
59
  print("\nSKILL.md 指令技能验证全部通过")
67
- # 注:OpenClaw 兼容段(原 [8]-[12])已拆到 test_markdown_skills_openclaw.py。
68
- # 原因:单进程内 3×TestClient lifespan + 4×Foxclaw 实例 + 两个不同 ST 模型共存时,
69
- # 第三次 lifespan 预热阶段 torch/FAISS 必现 0xC0000005 进程崩溃(基线对照实锤,
70
- # 与业务改动无关)。进程隔离后两段各自全绿。
71
60
 
72
61
  if __name__ == "__main__":
73
62
  test_markdown_skills()
@@ -17,7 +17,6 @@ def test_markdown_skills_openclaw():
17
17
  from fastapi.testclient import TestClient
18
18
  from foxclaw.api import app
19
19
 
20
- # 8. OpenClaw 兼容:无 triggers 的原味技能(单 .md)→ 装成功 + 描述相关触发
21
20
  NO_TRIG = "---\nname: skill-vetter\nversion: 1.0.0\ndescription: Security-first skill vetting. Use before installing any skill from ClawdHub or GitHub.\n---\nCheck red flags before install."
22
21
  with TestClient(app) as c:
23
22
  r = c.post("/api/ahs/md-skills", json={"content": NO_TRIG, "id": "skill_vetter", "acknowledged": True})
@@ -32,37 +31,33 @@ def test_markdown_skills_openclaw():
32
31
  z.writestr("_meta.json", _json.dumps({"slug": "skill-vetter-zip", "version": "1.0.0"}))
33
32
  _pkg = _b64.b64encode(_buf.getvalue()).decode()
34
33
  with TestClient(app) as c:
35
- # 9. zip 包上传(base64)→ id 用 slug、version 带出
36
34
  r = c.post("/api/ahs/md-skills", json={"package_b64": _pkg, "acknowledged": True})
37
35
  assert r.status_code == 200, r.text
38
36
  assert r.json()["skill"]["id"] == "skill-vetter-zip" and r.json()["skill"]["version"] == "1.0.0"
39
37
  print("[9] OpenClaw zip 包上传(slug→id,version) OK")
40
- # 10. zip 无 SKILL.md → 400
41
38
  _b2 = _io.BytesIO()
42
39
  with _zip.ZipFile(_b2, "w") as z: z.writestr("readme.txt", "hi")
43
40
  r = c.post("/api/ahs/md-skills", json={"package_b64": _b64.b64encode(_b2.getvalue()).decode(), "acknowledged": True})
44
41
  assert r.status_code == 400
45
42
  print("[10] zip 无 SKILL.md → 400 OK")
46
43
 
47
- # 11. 描述相关注入(无 triggers)+ 持久化 round-trip
48
44
  from foxclaw import Foxclaw
49
45
  os3 = Foxclaw(data_dir=_DATA)
50
- assert os3.md_skills.get("skill-vetter-zip") is not None # 持久化含 zip 装的
46
+ assert os3.md_skills.get("skill-vetter-zip") is not None
51
47
  ag3 = os3.supervisor
52
48
  hit = ag3._skill_augment("please vet this skill before install", [{"role": "user", "content": "please vet this skill before install"}])
53
- assert "Check red flags" in hit[-1]["content"] # 描述相关命中注入
49
+ assert "Check red flags" in hit[-1]["content"]
54
50
  miss = ag3._skill_augment("订一张机票", [{"role": "user", "content": "订一张机票"}])
55
- assert miss[0]["content"] == "订一张机票" # 无关不注入
51
+ assert miss[0]["content"] == "订一张机票"
56
52
  print("[11] 无 triggers 描述相关注入 + 持久化 OK")
57
53
 
58
- # 12. 编辑触发词(免解压重打包):PATCH triggers → 中文任务也能触发
59
54
  with TestClient(app) as c:
60
55
  r = c.patch("/api/ahs/md-skills/skill-vetter-zip", json={"triggers": ["审核", "搜索"]})
61
56
  assert r.status_code == 200 and r.json()["skill"]["triggers"] == ["审核", "搜索"], r.text
62
57
  os4 = Foxclaw(data_dir=_DATA)
63
58
  ag4 = os4.supervisor
64
59
  hit = ag4._skill_augment("帮我审核这个技能", [{"role": "user", "content": "帮我审核这个技能"}])
65
- assert "Check red flags" in hit[-1]["content"] # 加了中文 triggers → 中文任务命中
60
+ assert "Check red flags" in hit[-1]["content"]
66
61
  print("[12] 编辑触发词 → 中文任务触发 OK")
67
62
 
68
63
  print("\nOpenClaw 兼容验证全部通过")
@@ -5,8 +5,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
5
5
 
6
6
  def test_mcp_connectors():
7
7
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
8
- os.environ["FOXCLAW_SKIP_DOTENV"] = "1" # 不加载开发机 .env(否则 FOXCLAW_API_KEY 会开启鉴权导致 401)
9
- os.environ["HF_HUB_OFFLINE"] = "1" # 无网环境:跳过 huggingface 联网重试,直接用本地缓存
8
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
9
+ os.environ["HF_HUB_OFFLINE"] = "1"
10
10
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
11
11
  _DATA = tempfile.mkdtemp(prefix="foxclaw_mcp_")
12
12
  os.environ["FOXCLAW_DATA_DIR"] = _DATA
@@ -16,8 +16,6 @@ def test_mcp_connectors():
16
16
  import foxclaw.skills.mcp_client as mc
17
17
  from types import SimpleNamespace as NS
18
18
 
19
- # 客户端 SSRF 防线(用真实 _amcp_list_tools,monkeypatch 之前测):连内网/元数据 host
20
- # 应被请求钩子在发包前拦截(防恶意 MCP server 用 307 把你重定向进内网/云元数据)。
21
19
  import asyncio as _aio
22
20
  _blocked = False
23
21
  try:
@@ -42,46 +40,38 @@ def test_mcp_connectors():
42
40
  with TestClient(app) as c:
43
41
  print("=" * 60); print("工具中心 · MCP 连接器 验证"); print("=" * 60)
44
42
 
45
- # 1. 加服务器(https, ack) → 注册 2 工具
46
43
  r = c.post("/api/ahs/mcp/servers", json=SRV)
47
44
  assert r.status_code == 200, r.text
48
45
  assert r.json()["tools_registered"] == 2
49
46
  print("[1] 加服务器 + 注册 2 工具 OK")
50
47
 
51
- # 2. 技能出现 + fail-closed side_effect
52
48
  skills = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
53
49
  assert skills["mcp__demo_srv__get_time"]["side_effect"] == "read"
54
- assert skills["mcp__demo_srv__do_thing"]["side_effect"] == "write" # fail-closed
50
+ assert skills["mcp__demo_srv__do_thing"]["side_effect"] == "write"
55
51
  print("[2] 工具成技能 + fail-closed(read/write) OK")
56
52
 
57
- # 3. 服务器列表 tool_count=2
58
53
  srvs = c.get("/api/ahs/mcp/servers").json()["servers"]
59
54
  assert srvs and srvs[0]["tool_count"] == 2
60
55
  print("[3] 服务器列表 tool_count=2 OK")
61
56
 
62
- # 4. 审核门 deny 内网
63
57
  r = c.post("/api/ahs/mcp/servers", json={"id": "bad", "url": "http://127.0.0.1/mcp"})
64
58
  assert r.status_code == 403, r.text
65
59
  print("[4] 审核门 deny 内网(403) OK")
66
60
 
67
- # 5. 外部低信任未 ack → 409
68
61
  r = c.post("/api/ahs/mcp/servers", json={"id": "p2", "url": "https://mcp2.example.com/mcp"})
69
62
  assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
70
63
  print("[5] 外部低信任需确认(409) OK")
71
64
 
72
- # 6. 测试调用 mcp 技能:external 外部调用 → 先 needs_confirmation,确认后成功
73
65
  r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}})
74
66
  assert r.json()["status"] == "needs_confirmation", r.text
75
67
  r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}, "confirmed": True})
76
68
  j = r.json()
77
69
  assert r.status_code == 200 and j["success"] is True, r.text
78
- # 双层防提示注入:结构标记 _untrusted + 内容内联安全警告(LLM 注入时看得到)
79
70
  out = j["output"]
80
71
  assert out.get("_untrusted") is True, out
81
72
  assert "不可信" in out.get("data", {}).get("content", ""), out
82
73
  print("[6] 测试调用 mcp 技能 + 双层 untrusted 标记(防提示注入) OK")
83
74
 
84
- # 7. 连接失败 → 502
85
75
  async def boom(url, headers, transport): raise RuntimeError("connrefused")
86
76
  mc._amcp_list_tools = boom
87
77
  r = c.post("/api/ahs/mcp/servers", json={"id": "err_srv", "url": "https://mcp3.example.com/mcp", "acknowledged": True})
@@ -89,12 +79,10 @@ def test_mcp_connectors():
89
79
  mc._amcp_list_tools = fake_list
90
80
  print("[7] 连接失败 502 OK")
91
81
 
92
- # 8. 移除服务器 → 技能消失
93
82
  assert c.delete("/api/ahs/mcp/servers/demo_srv").status_code == 200
94
83
  assert "mcp__demo_srv__get_time" not in {s["id"] for s in c.get("/api/ahs/skills").json()["skills"]}
95
84
  print("[8] 移除服务器 + 技能消失 OK")
96
85
 
97
- # 9. 持久化 round-trip
98
86
  with TestClient(app) as c:
99
87
  c.post("/api/ahs/mcp/servers", json=SRV)
100
88
  from foxclaw import Foxclaw
@@ -8,17 +8,15 @@ def test_mcp_guardian_discovery():
8
8
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
9
9
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_mcp_guardian_")
10
10
 
11
- # 1. tool_to_spec 兼容 dict(守门人序列化产物),fail-closed 不变
12
11
  from foxclaw.skills.mcp_client import tool_to_spec
13
12
  spec = tool_to_spec("srv", {"name": "get_time", "description": "读时间", "inputSchema": {},
14
13
  "annotations": {"readOnlyHint": True, "destructiveHint": None}})
15
14
  assert spec.id == "mcp__srv__get_time" and spec.side_effect == "read" and not spec.destructive
16
15
  spec2 = tool_to_spec("srv", {"name": "do_thing", "description": "", "inputSchema": {},
17
16
  "annotations": None})
18
- assert spec2.side_effect == "write" # fail-closed:无注解按有副作用
17
+ assert spec2.side_effect == "write"
19
18
  print("[1] tool_to_spec 兼容 dict + fail-closed OK")
20
19
 
21
- # 2. 守门人 mcp_register:主进程空 headers 不覆盖守门人已有凭证
22
20
  import foxclaw.kernel.sentinel as sm
23
21
  servers = {}
24
22
  sm._handle_mcp_register({"servers": [
@@ -30,7 +28,6 @@ def test_mcp_guardian_discovery():
30
28
  assert servers["a"]["headers"]["Authorization"] == "Bearer SECRET", "空 headers 不应覆盖守门人已有凭证"
31
29
  print("[2] mcp_register 空 headers 保留旧凭证 OK")
32
30
 
33
- # 3. 守门人 mcp_list_tools 代发现:用自己的 headers 连 + 序列化为 dict
34
31
  import foxclaw.skills.mcp_client as mc
35
32
  _seen = {}
36
33
  async def fake_list(url, headers, transport):
@@ -41,10 +38,9 @@ def test_mcp_guardian_discovery():
41
38
  out = sm._handle_mcp_list_tools({"server_id": "a"}, servers)
42
39
  assert out["tools"][0]["name"] == "t1"
43
40
  assert out["tools"][0]["annotations"]["readOnlyHint"] is True
44
- assert _seen["headers"].get("Authorization") == "Bearer SECRET" # 守门人用自己的凭证连
41
+ assert _seen["headers"].get("Authorization") == "Bearer SECRET"
45
42
  print("[3] mcp_list_tools 守门人代发现 + 序列化 + 凭证不出守门人 OK")
46
43
 
47
- # 4. mcp_add_server_guardian:主进程不存 headers,守门人收到 headers
48
44
  class FakeSentinel:
49
45
  def __init__(self):
50
46
  self.registered = None
@@ -6,9 +6,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
6
6
 
7
7
  def test_mcp_no_memory():
8
8
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
9
- os.environ["FOXCLAW_SKIP_DOTENV"] = "1" # 隔离开发机 .env,保证测试环境自洽
9
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
10
10
  os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
11
- os.environ["HF_HUB_OFFLINE"] = "1" # 无网环境:跳过 huggingface 联网重试,直接用本地缓存
11
+ os.environ["HF_HUB_OFFLINE"] = "1"
12
12
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
13
13
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_nomem_")
14
14
 
@@ -16,7 +16,7 @@ def test_mcp_no_memory():
16
16
 
17
17
  o = Foxclaw(data_dir=os.environ["FOXCLAW_DATA_DIR"])
18
18
  calls = []
19
- o.hmr.ingest = lambda *a, **k: calls.append(1) # spy:数 HMR 写入次数
19
+ o.hmr.ingest = lambda *a, **k: calls.append(1)
20
20
 
21
21
 
22
22
  async def dispatch_suppressed():
@@ -24,7 +24,6 @@ class _StubGate:
24
24
  taint_level=0, taint_threshold=0):
25
25
  assert skill.side_effect == "write" and skill.external is True
26
26
  assert confirmed is False, "外部驱动不能用 confirmed 自证"
27
- # Verdict 的字段是 level(allow|warn|deny|confirm),不是 decision
28
27
  return SimpleNamespace(level=self.decision, reason="stub",
29
28
  raw=SimpleNamespace(reason_code=self.code),
30
29
  decision_id="d")