foxclaw 0.1.2 → 0.1.4

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 (368) 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/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -21,23 +21,20 @@ async def main():
21
21
  print("=" * 60)
22
22
  print("Skill 接入层 · 助手万能工具箱 + 对外兼容层")
23
23
  print("=" * 60)
24
- _data = tempfile.mkdtemp(prefix="foxclaw_skill_") # 系统临时目录,不污染仓库根
24
+ _data = tempfile.mkdtemp(prefix="foxclaw_skill_")
25
25
  os_ = Foxclaw(data_dir=_data, shadow_rounds=50)
26
26
  asst = os_.supervisor
27
27
 
28
- # 1. 列出可用 skill
29
28
  print("\n【1】助手可用 skill 目录")
30
29
  for s in asst.list_skills():
31
30
  print(f" · {s['name']:<10} [{s['kind']}/{s['side_effect']}] "
32
31
  f"触发: {','.join(s['triggers'][:3])}")
33
32
 
34
- # 2. 本地只读 skill:计算器(直接执行)
35
33
  print("\n【2】本地只读 skill(直接执行)")
36
34
  r = asst.use_skill("帮我计算 (12+8)*5", args={"expression": "(12+8)*5"})
37
35
  print(f" 匹配: {r['matched']['name']}")
38
36
  print(f" 结果: {r['result']['output']} · 状态: {r['result']['status']}")
39
37
 
40
- # 3. 有副作用 skill:发邮件(守门人拦截)
41
38
  print("\n【3】有副作用 skill(守门人拦截,需确认)")
42
39
  r2 = asst.use_skill("帮我发邮件给客户",
43
40
  args={"to": "a@b.com", "subject": "你好", "body": "..."})
@@ -45,7 +42,6 @@ async def main():
45
42
  print(f" 状态: {r2['result']['status']}")
46
43
  print(f" 守门人: {r2['result']['message']}")
47
44
 
48
- # 4. 确认后放行(MCP 未连 → not_connected)
49
45
  print("\n【4】用户确认后放行")
50
46
  r3 = asst.use_skill("帮我发邮件给客户",
51
47
  args={"to": "a@b.com", "subject": "你好", "body": "..."},
@@ -53,7 +49,6 @@ async def main():
53
49
  print(f" 状态: {r3['result']['status']}")
54
50
  print(f" 说明: {r3['result']['message']}")
55
51
 
56
- # 5. MCP 只读 skill:高德路线(未连接)
57
52
  print("\n【5】MCP skill 未连接(留接口)")
58
53
  r4 = asst.use_skill("从公司到机场怎么走",
59
54
  args={"origin": "公司", "destination": "机场", "mode": "driving"})
@@ -61,7 +56,6 @@ async def main():
61
56
  print(f" 状态: {r4['result']['status']}")
62
57
  print(f" 说明: {r4['result']['message']}")
63
58
 
64
- # 6. 深审回归:HookManager.audit_log 有界(长跑不泄漏)
65
59
  print("\n【6】深审回归:钩子审计日志有界(长跑不泄漏)")
66
60
  from foxclaw.skills.lifecycle import HookManager
67
61
  hm = HookManager(audit_limit=100)
@@ -89,39 +83,29 @@ def test_calc_hardening():
89
83
  """
90
84
  from foxclaw.skills import _demo_calc
91
85
 
92
- # 正常路径不受影响
93
86
  assert _demo_calc("50*2")["result"] == 100
94
87
  assert _demo_calc("(12+8)*5")["result"] == 100
95
88
  assert _demo_calc("7/2")["result"] == 3.5
96
- assert _demo_calc("2**100")["result"] == 2 ** 100 # 指数 100 是允许的边界
89
+ assert _demo_calc("2**100")["result"] == 2 ** 100
97
90
 
98
- # 1. 字符白名单(旧有闸):字母/下划线被禁,取名字取属性进不来
99
91
  assert "error" in _demo_calc("__import__('os')")
100
92
  assert "error" in _demo_calc("a+b")
101
93
  assert "error" in _demo_calc("1 .bit_length")
102
94
 
103
- # 2. 长度闸:>200 字符直接拒绝,不进 eval
104
95
  long_expr = "1+" * 100 + "1"
105
96
  assert len(long_expr) == 201
106
97
  assert _demo_calc(long_expr)["error"] == "表达式过长"
107
98
 
108
- # 3. 连续乘方闸:右结合会得到天文级指数,必须先拦
109
99
  assert _demo_calc("9**9**9")["error"] == "不支持连续乘方"
110
100
 
111
- # 4. 指数闸:必须是不超过 100 的整数
112
101
  assert _demo_calc("2**101")["error"] == "乘方的指数需为不超过 100 的整数"
113
102
  assert _demo_calc("2**999999")["error"] == "乘方的指数需为不超过 100 的整数"
114
103
  assert _demo_calc("2**1.5")["error"] == "乘方的指数需为不超过 100 的整数"
115
- # 已知限制:带括号/运算的指数一律拒绝(宁可少算,不放行难校验的形态)
116
104
  assert _demo_calc("2**(3)")["error"] == "乘方的指数需为不超过 100 的整数"
117
105
 
118
- # 5. 求值期异常降级为 error,不向上抛
119
106
  assert "error" in _demo_calc("1/0")
120
107
  assert "error" in _demo_calc("")
121
108
 
122
- # 6. 结果规模兜底(>4096 bit 的 int、非有限 float)是防御性检查:
123
- # 上面三道闸之后已不可达(无字母造不出 inf,指数 ≤100 造不出 4096 bit),
124
- # 故此处只确认正常结果不被误伤,不给兜底本身造假用例。
125
109
  assert "error" not in _demo_calc("2**100")
126
110
 
127
111
 
@@ -0,0 +1,282 @@
1
+ """`foxclaw skills install` 的来源解析与落地。
2
+
3
+ 全部离线:远端用 monkeypatch 替掉 `_http_get`,不打真网 —— 否则 CI 会因 ClawHub
4
+ 抖动而红,这类测试一旦不稳就会被跳过,等于没测。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import io
9
+ import json
10
+ import tarfile
11
+ import zipfile
12
+
13
+ import pytest
14
+
15
+ from foxclaw.skills import fetch as fetch_mod
16
+ from foxclaw.skills.archive import PackageError, has_code, read_package
17
+
18
+ MD = "---\nname: Demo Skill\ndescription: 演示用技能\nversion: 1.2.3\n---\n\n正文指示"
19
+
20
+
21
+ def _zip(files: dict) -> bytes:
22
+ buf = io.BytesIO()
23
+ with zipfile.ZipFile(buf, "w") as z:
24
+ for name, body in files.items():
25
+ z.writestr(name, body)
26
+ return buf.getvalue()
27
+
28
+
29
+ def _tgz(files: dict) -> bytes:
30
+ buf = io.BytesIO()
31
+ with tarfile.open(fileobj=buf, mode="w:gz") as t:
32
+ for name, text in files.items():
33
+ raw = text.encode("utf-8")
34
+ info = tarfile.TarInfo(name)
35
+ info.size = len(raw)
36
+ t.addfile(info, io.BytesIO(raw))
37
+ return buf.getvalue()
38
+
39
+
40
+ def _mock_http(monkeypatch, routes: dict) -> None:
41
+ def fake(url: str, timeout: float = 25.0, max_bytes: int = 10 * 1024 * 1024):
42
+ for key, val in routes.items():
43
+ if key in url:
44
+ return val
45
+ raise PackageError(f"测试未配置这条路由: {url}")
46
+
47
+ monkeypatch.setattr(fetch_mod, "_http_get", fake)
48
+
49
+
50
+ def test_read_zip_root_skill_md():
51
+ pkg = read_package(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "demo"}'}))
52
+ assert pkg["skill_md"] == MD
53
+ assert pkg["meta"]["slug"] == "demo"
54
+
55
+
56
+ def test_read_targz_picks_shallowest():
57
+ pkg = read_package(_tgz({
58
+ "repo-abc/SKILL.md": MD,
59
+ "repo-abc/docs/SKILL.md": "更深的同名副本应被忽略",
60
+ }))
61
+ assert pkg["skill_md"] == MD
62
+
63
+
64
+ def test_read_package_rejects_traversal():
65
+ with pytest.raises(PackageError):
66
+ read_package(_zip({"../evil/SKILL.md": MD}))
67
+
68
+
69
+ def test_read_package_without_skill_md():
70
+ with pytest.raises(PackageError, match="未找到 SKILL.md"):
71
+ read_package(_zip({"README.md": "nothing here"}))
72
+
73
+
74
+ def test_subpath_narrows_search():
75
+ with pytest.raises(PackageError):
76
+ read_package(_tgz({"repo/a/SKILL.md": MD}), subpath="other")
77
+
78
+
79
+ def test_has_code_detects_scripts():
80
+ assert has_code(["SKILL.md", "scripts/run.py"])
81
+ assert has_code(["SKILL.md", "tool.sh"])
82
+ assert not has_code(["SKILL.md", "skill-card.md"])
83
+
84
+
85
+ def test_ref_routes_scoped_name_to_clawhub(monkeypatch):
86
+ seen: dict = {}
87
+
88
+ def fake(owner, slug, timeout):
89
+ seen["id"] = (owner, slug)
90
+ return fetch_mod.SkillPackage(skill_md=MD, source="clawhub", origin="x")
91
+
92
+ monkeypatch.setattr(fetch_mod, "_from_clawhub", fake)
93
+ assert fetch_mod.fetch("@snowfox/demo").origin == "x"
94
+ assert seen["id"] == ("snowfox", "demo")
95
+ fetch_mod.fetch("snowfox/demo")
96
+ assert seen["id"] == ("snowfox", "demo")
97
+
98
+
99
+ def test_unknown_ref_rejected():
100
+ with pytest.raises(PackageError, match="无法识别"):
101
+ fetch_mod.fetch("no-such-thing-xyz")
102
+
103
+
104
+ def test_local_dir_reads_skill_md(tmp_path):
105
+ (tmp_path / "SKILL.md").write_text(MD, encoding="utf-8")
106
+ pkg = fetch_mod.fetch(str(tmp_path))
107
+ assert pkg.source == "local"
108
+ assert "演示用技能" in pkg.skill_md
109
+
110
+
111
+ def test_local_dir_without_skill_md(tmp_path):
112
+ with pytest.raises(PackageError):
113
+ fetch_mod.fetch(str(tmp_path))
114
+
115
+
116
+ def test_clawhub_blocks_malware(monkeypatch):
117
+ _mock_http(monkeypatch, {
118
+ "/api/v1/skills/": (json.dumps({
119
+ "moderation": {"isMalwareBlocked": True, "summary": "窃取密钥"},
120
+ "latestVersion": {"version": "1.0.0"},
121
+ }).encode(), "application/json"),
122
+ })
123
+ with pytest.raises(fetch_mod.MalwareBlocked):
124
+ fetch_mod.fetch("@bad/skill")
125
+
126
+
127
+ def test_clawhub_github_handoff(monkeypatch):
128
+ """download 返回 JSON 交接(GitHub 托底)时要改下 archiveUrl,而不是当 zip 解。"""
129
+ _mock_http(monkeypatch, {
130
+ "/api/v1/skills/": (json.dumps({
131
+ "moderation": {"isSuspicious": True},
132
+ "latestVersion": {"version": "2.0.0"},
133
+ }).encode(), "application/json"),
134
+ "/api/v1/download": (json.dumps({
135
+ "archiveUrl": "https://codeload.example/archive.tar.gz",
136
+ }).encode(), "application/json"),
137
+ "archive.tar.gz": (_tgz({"repo-main/SKILL.md": MD}), "application/gzip"),
138
+ })
139
+ pkg = fetch_mod.fetch("@snowfox/demo")
140
+ assert pkg.version == "2.0.0"
141
+ assert pkg.suspicious is True
142
+ assert "repo-main/SKILL.md" in pkg.files
143
+
144
+
145
+ def test_clawhub_not_found(monkeypatch):
146
+ def boom(url, timeout=25.0, max_bytes=10 * 1024 * 1024):
147
+ raise fetch_mod.HttpError(404, url, "Not found")
148
+
149
+ monkeypatch.setattr(fetch_mod, "_http_get", boom)
150
+ with pytest.raises(PackageError, match="找不到"):
151
+ fetch_mod.fetch("@ghost/missing")
152
+
153
+
154
+ def test_github_subpath(monkeypatch):
155
+ _mock_http(monkeypatch, {
156
+ "codeload.github.com": (_tgz({
157
+ "repo-main/skills/demo/SKILL.md": MD,
158
+ "repo-main/skills/other/SKILL.md": "x",
159
+ }), "application/gzip"),
160
+ })
161
+ pkg = fetch_mod.fetch("github:snowfox/repo/skills/demo@main")
162
+ assert pkg.source == "github"
163
+ assert pkg.skill_md == MD
164
+ assert pkg.origin == "github:snowfox/repo/skills/demo@main"
165
+
166
+
167
+ def _src(tmp_path):
168
+ src = tmp_path / "src"
169
+ src.mkdir()
170
+ (src / "SKILL.md").write_text(MD, encoding="utf-8")
171
+ return src
172
+
173
+
174
+ def test_cli_install_writes_file_when_service_down(tmp_path, monkeypatch):
175
+ from foxclaw import cli
176
+ from foxclaw.skills import install_cmd
177
+
178
+ data = tmp_path / "data"
179
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
180
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
181
+
182
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
183
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
184
+ assert [s["id"] for s in saved["skills"]] == ["demo_skill"]
185
+ assert saved["skills"][0]["version"] == "1.2.3"
186
+
187
+
188
+ def test_cli_install_respects_explicit_id(tmp_path, monkeypatch):
189
+ from foxclaw import cli
190
+ from foxclaw.skills import install_cmd
191
+
192
+ data = tmp_path / "data"
193
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
194
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
195
+
196
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes",
197
+ "--id", "my_demo"]) == 0
198
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
199
+ assert saved["skills"][0]["id"] == "my_demo"
200
+
201
+
202
+ def test_cli_install_refuses_without_confirmation(tmp_path, monkeypatch):
203
+ from foxclaw import cli
204
+ from foxclaw.skills import install_cmd
205
+
206
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
207
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
208
+ monkeypatch.setattr(install_cmd, "_confirm", lambda: False)
209
+
210
+ assert cli.main(["skills", "install", str(_src(tmp_path))]) == 1
211
+ assert not (tmp_path / "data" / "markdown_skills.json").exists()
212
+
213
+
214
+ def test_cli_list_reads_file_when_service_down(tmp_path, monkeypatch, capsys):
215
+ from foxclaw import cli
216
+ from foxclaw.skills import install_cmd
217
+
218
+ data = tmp_path / "data"
219
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
220
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
221
+ cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
222
+ capsys.readouterr()
223
+
224
+ assert cli.main(["skills", "list", "--json"]) == 0
225
+ got = json.loads(capsys.readouterr().out)
226
+ assert got["source"] == "file"
227
+ assert got["skills"][0]["id"] == "demo_skill"
228
+
229
+
230
+ def test_cli_remove_deletes_skill(tmp_path, monkeypatch):
231
+ from foxclaw import cli
232
+ from foxclaw.skills import install_cmd
233
+
234
+ data = tmp_path / "data"
235
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(data))
236
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
237
+ cli.main(["skills", "install", str(_src(tmp_path)), "--yes"])
238
+
239
+ assert cli.main(["skills", "remove", "demo_skill"]) == 0
240
+ saved = json.loads((data / "markdown_skills.json").read_text(encoding="utf-8"))
241
+ assert saved["skills"] == []
242
+
243
+
244
+ def test_cli_remove_unknown_id_fails(tmp_path, monkeypatch):
245
+ from foxclaw import cli
246
+ from foxclaw.skills import install_cmd
247
+
248
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
249
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: False)
250
+ assert cli.main(["skills", "remove", "nope"]) == 1
251
+
252
+
253
+ def test_cli_install_uses_api_when_service_alive(tmp_path, monkeypatch):
254
+ """服务在跑时必须打 API:直接改 json 会被运行中注册表的下次 save 整份覆盖。"""
255
+ from foxclaw import cli
256
+ from foxclaw.skills import install_cmd
257
+
258
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path / "data"))
259
+ monkeypatch.setattr(install_cmd, "_api_alive", lambda base: True)
260
+ calls: list = []
261
+
262
+ def fake_json(base, method, path, payload=None):
263
+ calls.append((method, path, payload))
264
+ return {"created": True}
265
+
266
+ monkeypatch.setattr(install_cmd, "_api_json", fake_json)
267
+
268
+ assert cli.main(["skills", "install", str(_src(tmp_path)), "--yes"]) == 0
269
+ method, path, payload = calls[0]
270
+ assert (method, path) == ("POST", "/api/ahs/md-skills")
271
+ assert payload["acknowledged"] is True
272
+ assert payload["id"] == "demo_skill"
273
+ assert "正文指示" in payload["content"]
274
+ assert not (tmp_path / "data" / "markdown_skills.json").exists()
275
+
276
+
277
+ def test_local_zip_package(tmp_path):
278
+ p = tmp_path / "demo.zip"
279
+ p.write_bytes(_zip({"SKILL.md": MD, "_meta.json": '{"slug": "pack"}'}))
280
+ pkg = fetch_mod.fetch(str(p))
281
+ assert pkg.source == "local"
282
+ assert pkg.meta["slug"] == "pack"
@@ -33,14 +33,13 @@ def _adapter_with(hmr_obj):
33
33
  return s
34
34
 
35
35
 
36
- # ── 1. 记忆统计 ────────────────────────────────────────────────────────────────
37
36
  def test_stats_counts_from_memory_fs_not_vector_store():
38
37
  """核心回归:记忆在 memory_fs;vector_store 没有 count/size/memories 也不能让 total 归零。"""
39
38
  mems = [_mem(["agent:supervisor"]), _mem(["agent:supervisor"]),
40
39
  _mem(["agent:finance", "shared"]), _mem(["agent:office"])]
41
40
  hmr = types.SimpleNamespace(
42
41
  memory_fs=types.SimpleNamespace(list_memories=lambda: mems),
43
- vector_store=types.SimpleNamespace(), # 故意:没有 count/size/memories(复现旧 bug 条件)
42
+ vector_store=types.SimpleNamespace(),
44
43
  memory_graph=None)
45
44
  out = HMRAdapter.stats(_adapter_with(hmr))
46
45
  assert out["total_memories"] == 4, "必须从 memory_fs 数出真实记忆数"
@@ -75,14 +74,13 @@ def test_stats_survives_broken_hmr():
75
74
  assert out["total_memories"] == 0 and out["relations"] == 0
76
75
 
77
76
 
78
- # ── 1b. 记忆标题可读性 ─────────────────────────────────────────────────────────
79
77
  def test_derive_title_from_content():
80
78
  d = HMRAdapter._derive_title
81
79
  assert d("对话:你好 → 你好呀!周五中午,正是一周里…") == "你好"
82
80
  assert d("用户偏好:我喜欢简洁的回答") == "我喜欢简洁的回答"
83
81
  assert d("把这份报告存入HMR → 好的,我现在") == "把这份报告存入HMR"
84
82
  assert d("") == ""
85
- assert len(d("很长" * 50)) <= 24 # 限长
83
+ assert len(d("很长" * 50)) <= 24
86
84
 
87
85
 
88
86
  def test_recent_replaces_timestamp_titles():
@@ -109,7 +107,6 @@ def test_recent_keeps_meaningful_title():
109
107
  assert s.recent(5)[0]["title"] == "季度财务复盘"
110
108
 
111
109
 
112
- # ── 2. 活动事件 ────────────────────────────────────────────────────────────────
113
110
  def test_chat_publishes_activity_event(monkeypatch):
114
111
  """核心回归:对话完成后必须往事件脊柱发 chat.completed,否则"活动"页永远空。"""
115
112
  monkeypatch.setenv("FOXCLAW_TOOL_USE", "1")
@@ -6,26 +6,22 @@ import pytest
6
6
 
7
7
 
8
8
  def test_streaming():
9
- # ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
10
- # 假红。全量请用 tests/run_all.py(每文件独立子进程)。
11
9
  if "foxclaw.api" in sys.modules:
12
10
  pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
13
11
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
14
12
  os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
15
- os.environ["FOXCLAW_STREAM_RATE"] = "1" # 令牌桶=1,测限流
13
+ os.environ["FOXCLAW_STREAM_RATE"] = "1"
16
14
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_stream_")
17
15
 
18
16
  print("=" * 60); print("LLM 流式输出验证"); print("=" * 60)
19
17
 
20
- # 1) router.stream(stub 模式):分块拼接 == 完整文本、多块
21
18
  from foxclaw.core.model_router import ModelRouter
22
- r = ModelRouter() # 无 key → stub
19
+ r = ModelRouter()
23
20
  chunks = list(r.stream("sys", [{"role": "user", "content": "你好世界这是一段用于测试流式分块的文本"}]))
24
21
  assert len(chunks) > 1, f"应多块: {len(chunks)}"
25
22
  assert "".join(chunks), "拼接应非空"
26
23
  print(f"[1] router.stream stub 分块(chunk={len(chunks)}) OK")
27
24
 
28
- # 2) /api/chat/stream SSE:多个 delta + done 收尾
29
25
  from fastapi.testclient import TestClient
30
26
  from foxclaw.api import app
31
27
  with TestClient(app) as c:
@@ -46,7 +42,6 @@ def test_streaming():
46
42
  assert "".join(deltas), "拼接应非空"
47
43
  print(f"[2] /api/chat/stream SSE(delta={len(deltas)}, done) OK")
48
44
 
49
- # 3) 限流:桶=1,上面 [2] 已消耗 → 再请求被限流
50
45
  with c.stream("POST", "/api/chat/stream",
51
46
  json={"task": "再来一次", "agent_id": "supervisor"}) as resp:
52
47
  txt = "".join(resp.iter_text())
@@ -26,7 +26,7 @@ from pathlib import Path
26
26
  sys.path.insert(0, str(Path(__file__).parent.parent))
27
27
 
28
28
  import foxclaw.core.web_search as ws
29
- ws.needs_search = lambda q: False # 自答/兜底路径不触发联网,保持离线
29
+ ws.needs_search = lambda q: False
30
30
 
31
31
  from foxclaw import Foxclaw
32
32
  from foxclaw.agents.base import CognitiveAgent, HarnessBlockedError
@@ -59,7 +59,7 @@ def main():
59
59
  check(_names() == EXPECTED_STAGES, f"主管 stage 序列={EXPECTED_STAGES}(实为 {_names()})")
60
60
 
61
61
  os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_svpipe_"))
62
- os_.router.provider = "stub" # 共享 router,五 Agent 同源 → 全离线确定性
62
+ os_.router.provider = "stub"
63
63
  os_.router._client = None
64
64
  sup = os_.supervisor
65
65
 
@@ -74,7 +74,7 @@ def main():
74
74
  "返回体由 base 模板统一组装(agent/session_id/thought_summary)")
75
75
 
76
76
  print("\n【3】单 Agent 分发:返回体顶层键与旧实现逐字一致")
77
- r = asyncio.run(sup.run_task("帮我做个预算分析")) # "预算" 命中 finance
77
+ r = asyncio.run(sup.run_task("帮我做个预算分析"))
78
78
  res = r["result"]
79
79
  check(res.get("dispatched_to") == "finance", f"关键词命中 → finance(实为 {res.get('dispatched_to')})")
80
80
  for k in ("schedule", "plan", "process_id"):
@@ -91,7 +91,7 @@ def main():
91
91
  print("\n【5】统一 execution.completed(文稿 Step 4)")
92
92
  spine = os_.event_spine
93
93
  before = len(spine.history("execution.completed"))
94
- asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文")) # 并行 3 子任务
94
+ asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文"))
95
95
  evts = spine.history("execution.completed")
96
96
  new_evts = evts[before:]
97
97
  check(len(new_evts) == 1,
@@ -112,7 +112,6 @@ def main():
112
112
  new_evts = spine.history("execution.completed")[before:]
113
113
  check(len(new_evts) == 1 and new_evts[0]["payload"].get("blocked") is True,
114
114
  "被拦任务的执行事件 blocked=True → 真值链不沉淀")
115
- # 还原 finance,别污染后续用例
116
115
  del os_.finance._execute
117
116
 
118
117
  print("\n【6】流式:run_task_stream 八 stage + 子 Agent token 传播 + 中断")
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  sys.path.insert(0, str(Path(__file__).parent.parent))
21
21
 
22
22
  import foxclaw.core.web_search as ws
23
- ws.needs_search = lambda q: False # 自答路径不触发联网,保持离线
23
+ ws.needs_search = lambda q: False
24
24
 
25
25
  from foxclaw import Foxclaw
26
26
 
@@ -31,8 +31,6 @@ def check(cond, msg):
31
31
  print(f" ✓ {msg}")
32
32
 
33
33
 
34
- # 分诊 prompt 的特征串。历史上 _llm_classify 用它问模型;现已无此路径,
35
- # 故断言它**从不出现**,即「不再为分诊多花一次 LLM 往返」。
36
34
  CLASSIFY_MARK = "只输出一个词"
37
35
 
38
36
 
@@ -47,19 +45,16 @@ def main():
47
45
  def fake(system, messages, max_tokens=2048, agent_id=""):
48
46
  content = str(messages[-1]["content"]) if messages else ""
49
47
  calls.append(content)
50
- if CLASSIFY_MARK in content: # 若真有人恢复分诊,这里会暴露
48
+ if CLASSIFY_MARK in content:
51
49
  return "finance"
52
50
  return "已处理"
53
51
 
54
- # 让路由认为模型可用(非 stub)
55
52
  os_.router.provider = "deepseek"
56
53
  os_.router._client = object()
57
54
  os_.router.complete = fake
58
55
  check(os_.router.is_stub is False, "router 处于'可用'状态(非 stub)")
59
56
 
60
57
  print("\n【1】关键词未命中 + 模型可用 → 主管直接自答(不再二次分诊)")
61
- # 这句不含任何领域关键词,也不像"花了钱"的报备(没有完成态动词),
62
- # 所以仍然走自答 —— 花费报备现在已算作财务命中,见【1b】。
63
58
  no_kw = "讲个笑话听听"
64
59
  check(os_.supervisor._has_keyword_hit(no_kw) is False, f"「{no_kw[:12]}…」确无关键词命中")
65
60
  r = asyncio.run(os_.supervisor.run_task(no_kw))
@@ -73,13 +68,12 @@ def main():
73
68
  for line in ("今天买了一件衣服花了105块", "后天花了100洗车"):
74
69
  check(os_.supervisor._has_keyword_hit(line), f"「{line[:12]}…」算作领域命中")
75
70
  check(os_.supervisor._route(line) == "finance", f"「{line[:12]}…」→ finance")
76
- # 讨论额度/比例不算报备,但仍属于财务问题 → 也交财务,而不是主管自答
77
71
  check(os_.supervisor._route("每月房租3000占收入10000这样合理吗") == "finance",
78
72
  "房租占比咨询 → finance")
79
73
 
80
74
  print("\n【2】关键词命中 → 直接路由,不问 LLM 分诊(省成本)")
81
75
  calls.clear()
82
- r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析")) # "预算" 命中 finance
76
+ r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析"))
83
77
  d2 = r2["result"].get("dispatched_to")
84
78
  check(d2 == "finance", f"命中'预算' → 直接 finance(实为 {d2})")
85
79
  check(not any(CLASSIFY_MARK in c for c in calls), "关键词命中时不发出分诊 prompt")
@@ -88,7 +82,7 @@ def main():
88
82
  os_.router.provider = "stub"
89
83
  os_.router._client = None
90
84
  calls.clear()
91
- r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器")) # 无关键词
85
+ r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器"))
92
86
  check(os_.router.is_stub is True, "router 回到 stub")
93
87
  d3 = r3["result"].get("dispatched_to")
94
88
  check(d3 == "supervisor", f"stub 下未命中 → 主管自身处理(实为 {d3})")
@@ -36,18 +36,15 @@ def _skill(side_effect="read", read_only=True, external=False, destructive=False
36
36
  )
37
37
 
38
38
 
39
- # ── 1. taint_level 分级 ───────────────────────────────────────────────
40
39
  def test_taint_level_grading():
41
40
  assert IG.taint_level("web_search") == IG.TAINT_EXTERNAL == 1
42
41
  assert IG.taint_level("knowledge") == IG.TAINT_PERSONAL == 2
43
42
  assert IG.taint_level("file") == IG.TAINT_PERSONAL == 2
44
43
  assert IG.taint_level("memory") == IG.TAINT_SENSITIVE == 3
45
- assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL # 未知来源不低估
44
+ assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL
46
45
 
47
46
 
48
- # ── 2. permission 污点门槛 ─────────────────────────────────────────────
49
47
  def test_permission_taint_default_off_behavior_unchanged():
50
- # 不传污点参数(默认关闭)→ 外发工具走原有 Stage 4 副作用确认
51
48
  s = _skill(side_effect="send", read_only=False)
52
49
  d = PERM.evaluate(s, {})
53
50
  assert d.decision == "ask" and d.reason_code == PERM.SIDE_EFFECT_CONFIRM
@@ -77,19 +74,17 @@ def test_permission_taint_not_trigger_on_readonly():
77
74
 
78
75
  def test_permission_taint_not_trigger_below_threshold():
79
76
  s = _skill(side_effect="send", read_only=False)
80
- # 污点只有 EXTERNAL(1) < 阈值 PERSONAL(2)→ 不因污点拦,走副作用确认
81
77
  d = PERM.evaluate(s, {}, taint_level=IG.TAINT_EXTERNAL,
82
78
  taint_threshold=IG.TAINT_PERSONAL)
83
79
  assert d.reason_code == PERM.SIDE_EFFECT_CONFIRM
84
80
 
85
81
 
86
- # ── 3. base 污点传播 ───────────────────────────────────────────────────
87
82
  def test_taint_helpers_on_fake_agent():
88
83
  a = types.SimpleNamespace(_think=lambda **k: None)
89
84
  assert _taint_get(a) == 0
90
85
  _taint_raise(a, IG.TAINT_EXTERNAL)
91
86
  _taint_raise(a, IG.TAINT_SENSITIVE)
92
- assert _taint_get(a) == IG.TAINT_SENSITIVE # 单调不减
87
+ assert _taint_get(a) == IG.TAINT_SENSITIVE
93
88
  _taint_reset(a)
94
89
  assert _taint_get(a) == 0
95
90
 
@@ -9,11 +9,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
9
9
  def test_task_stream():
10
10
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
11
11
  os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
12
- os.environ["FOXCLAW_STREAM_RATE"] = "60" # 放开限流(限流由 test_streaming 覆盖),这里只验通路
12
+ os.environ["FOXCLAW_STREAM_RATE"] = "60"
13
13
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_taskstream_")
14
- # 强制 stub(离线/确定性/免费):关掉 .env,使进程环境变量成为唯一来源。
15
- # (旧写法是逐个把 key 置空以依赖 load_dotenv(override=False)——但那挡不住「先 pop 再
16
- # import」的路径,也挡不住 .env 里的 FOXCLAW_MCP_SERVER 等非 key 变量。见 api.py 说明。)
17
14
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
18
15
  for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
19
16
  "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
@@ -24,16 +21,11 @@ def test_task_stream():
24
21
  from fastapi.testclient import TestClient
25
22
  import foxclaw.api as _api
26
23
  from foxclaw.api import app
27
- # pytest 同进程下,test_streaming.py 可能已把模块级流式限流桶设为 rate=1(并耗尽),
28
- # 而本测试要放开限流跑两条流式通路 —— 复位桶容量/速率/余量(限流语义由 test_streaming 覆盖)。
29
24
  _api._STREAM_LIMITER.capacity = 60
30
25
  _api._STREAM_LIMITER.rate = 1.0
31
26
  _api._STREAM_LIMITER.tokens = 60.0
32
27
 
33
28
  with TestClient(app) as c:
34
- # 1) 正常流式:SSE 收到 stage 进度 + act delta + 恰一个 result,无 error
35
- # 拓扑 5 → 3:领域 Agent 才走 DEFAULT_STAGES(含 act),主管走路由管线
36
- # (recall→route→dispatch→aggregate),不会产出 act —— 故此处用 office。
37
29
  with c.stream("POST", "/api/task/stream",
38
30
  json={"task": "帮我分析一下本月开销", "agent_id": "office"}) as resp:
39
31
  assert resp.status_code == 200, resp.status_code
@@ -53,9 +45,6 @@ def test_task_stream():
53
45
  assert results[0].get("agent") == "office" and "result" in results[0], results[0]
54
46
  print(f"[1] /api/task/stream SSE(stage={len(stages)} delta={len(deltas)} result=1) OK")
55
47
 
56
- # 2) 【P3-3】主管不再回落领域 Agent:P1-1 pipeline 化后主管自身支持流式,
57
- # stage 事件是主管管线七阶段,result.agent=supervisor,路由结论照常(stub 无关键词
58
- # → 主管自身处理)。
59
48
  with c.stream("POST", "/api/task/stream",
60
49
  json={"task": "随便问问", "agent_id": "supervisor"}) as resp:
61
50
  assert resp.status_code == 200, resp.status_code
@@ -15,7 +15,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
15
15
 
16
16
 
17
17
  def test_chat_stream_emits_named_thinking_event():
18
- # ISOLATION_GUARD:依赖 import 期固化的单例(路由配置/限流器),须独立进程跑。
19
18
  if "foxclaw.api" in sys.modules:
20
19
  import pytest
21
20
  pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")