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
@@ -19,67 +19,56 @@ def test_tool_center():
19
19
  with TestClient(app) as c:
20
20
  print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
21
21
 
22
- # 1. 安装 web_api 技能(https 只读 → allow)
23
22
  r = c.post("/api/ahs/skills", json=WEBAPI)
24
23
  assert r.status_code == 200, r.text
25
24
  assert r.json()["created"] is True
26
25
  print("[1] 安装 web_api 技能 OK")
27
26
 
28
- # 2. 列表里出现且 source=user
29
27
  r = c.get("/api/ahs/skills"); assert r.status_code == 200
30
28
  ids = {s["id"]: s for s in r.json()["skills"]}
31
29
  assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
32
30
  print("[2] 列表含新技能且 source=user OK")
33
31
 
34
- # 3. 拒绝 local kind
35
32
  r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
36
33
  assert r.status_code == 400
37
34
  print("[3] 拒绝 local kind(400) OK")
38
35
 
39
- # 4. 拒绝非法 endpoint
40
36
  r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
41
37
  assert r.status_code == 400
42
38
  print("[4] 拒非法 endpoint(400) OK")
43
39
 
44
- # 5. mcp 缺字段
45
40
  r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
46
41
  assert r.status_code == 400
47
42
  print("[5] mcp 缺 server/tool(400) OK")
48
43
 
49
- # 6. 启停
50
44
  r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
51
45
  assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
52
46
  r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
53
47
  assert r.json()["skill"]["enabled"] is True
54
48
  print("[6] 启停切换 OK")
55
49
 
56
- # 7. 删内置 → 409(取列表里第一个 builtin)
57
50
  r = c.get("/api/ahs/skills")
58
51
  builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
59
52
  if builtin:
60
53
  rd = c.delete(f"/api/ahs/skills/{builtin}")
61
54
  assert rd.status_code == 409, rd.text
62
55
  print("[7] 内置技能不可删(409) OK")
63
- # 7b. 用内置 id 安装 → 拒(不能覆盖内置)
64
56
  rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
65
57
  assert rc.status_code == 409, rc.text
66
58
  print("[7b] 用内置 id 安装被拒(409) OK")
67
59
  else:
68
60
  print("[7] 跳过(无内置技能)")
69
61
 
70
- # 8. 测试调用:read 技能直接跑(返回 SkillResult 结构)
71
62
  r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
72
63
  assert r.status_code == 200 and "success" in r.json()
73
64
  print("[8] 测试调用返回 SkillResult OK")
74
65
 
75
- # 9. 安装审核门:打本地地址的 web_api 安装期被 deny(403)
76
66
  r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
77
67
  "triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
78
68
  assert r.status_code == 403, r.text
79
69
  assert r.json()["detail"]["error"] == "install_denied"
80
70
  print("[9] 安装审核门 deny 本地地址(403) OK")
81
71
 
82
- # 9b. 有副作用技能未确认 → 409 needs_ack;acknowledged 后 → 200(且持久,供步12 round-trip)
83
72
  _send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
84
73
  "triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
85
74
  r = c.post("/api/ahs/skills", json=_send)
@@ -88,19 +77,16 @@ def test_tool_center():
88
77
  assert r.status_code == 200, r.text
89
78
  print("[9b] 副作用技能需确认(409→200) OK")
90
79
 
91
- # 10. 治理端点可用
92
80
  assert c.get("/api/ahs/breaker").status_code == 200
93
81
  assert c.get("/api/ahs/hooks/audit").status_code == 200
94
82
  print("[10] 治理端点(breaker/audit) OK")
95
83
 
96
- # 10b. 信任分级:内置 calculator(local)=high、内置 mcp=low、用户 web_api=mid
97
84
  sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
98
85
  assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
99
- assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"] # 内置但 mcp→低
86
+ assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
100
87
  assert sk["wx_weather"]["trust"] == "mid"
101
88
  print("[10b] 信任分级 high/mid/low OK")
102
89
 
103
- # 10c. deny 封杀:封杀后测试调用被拦 + blocked 标;解封恢复
104
90
  c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
105
91
  r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
106
92
  assert r["status"] == "blocked" and "封杀" in r["message"], r
@@ -111,12 +97,10 @@ def test_tool_center():
111
97
  assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
112
98
  print("[10c] deny 封杀/解封 kill-switch OK")
113
99
 
114
- # 11. 删除用户技能
115
100
  assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
116
101
  r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
117
102
  print("[11] 删除用户技能 OK")
118
103
 
119
- # 12. 持久化 round-trip:直建新 OS(同数据目录),已安装的 send_x 应仍在
120
104
  from foxclaw import Foxclaw
121
105
  os2 = Foxclaw(data_dir=_DATA)
122
106
  assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"
@@ -43,7 +43,7 @@ def test_readonly_tools_run_concurrently(monkeypatch) -> None:
43
43
  barrier = threading.Barrier(2, timeout=3)
44
44
 
45
45
  def behave(name, args, sink):
46
- barrier.wait() # 串行时第二个永远到不了 → BrokenBarrierError
46
+ barrier.wait()
47
47
  return f"{name}:ok"
48
48
 
49
49
  fake = _make(monkeypatch, behave)
@@ -72,7 +72,7 @@ def test_slow_tool_is_cut_off(monkeypatch) -> None:
72
72
 
73
73
  def test_taint_collected_in_worker_and_merged_in_main(monkeypatch) -> None:
74
74
  def behave(name, args, sink):
75
- sink.append(3 if name == "web_search" else 2) # worker 只能收集,不能直接写会话污点
75
+ sink.append(3 if name == "web_search" else 2)
76
76
  return "ok"
77
77
 
78
78
  fake = _make(monkeypatch, behave)
@@ -46,13 +46,11 @@ def main():
46
46
  print("\n【3】类型不符被检出(C 类:静默失败改为可见)")
47
47
  errs = r.validate_args("web_search", {"query": {"a": 1}})
48
48
  check(any("类型" in e for e in errs), f"dict 传 string 位被检出({errs})")
49
- # top_k 声明在 recall_memory 上(不在 web_search),故用它验 integer 校验
50
49
  errs = r.validate_args("recall_memory", {"query": "x", "top_k": "ten"})
51
50
  check(any("类型" in e for e in errs), f"字符串传 integer 被检出({errs})")
52
51
  errs = r.validate_args("recall_memory", {"query": "x", "top_k": True})
53
52
  check(not any("top_k" in e for e in errs),
54
- f"bool 不被误判为 integer({errs})") # bool 是 int 子类,须排除
55
- # 未在 schema 中声明的额外字段应放过(不引入 jsonschema 依赖的宽松设计)
53
+ f"bool 不被误判为 integer({errs})")
56
54
  check(r.validate_args("web_search", {"query": "ok", "未声明字段": 1}) == [],
57
55
  "未声明的额外字段不报错(避免过度约束模型)")
58
56
 
@@ -82,8 +80,6 @@ def main():
82
80
  print("\n【6】参数解析失败不再静默(args_parse_failed 标记存在)")
83
81
  import inspect
84
82
  from foxclaw.core.model_router import ModelRouter
85
- # 检查整个类:标记可能随重构从 call_tools 移到 _call_openai_tools 等子方法,
86
- # 锁「类内存在该标记」而非锁某个方法名(避免重构后误报)。
87
83
  src = inspect.getsource(ModelRouter)
88
84
  check("args_parse_failed" in src,
89
85
  "call_tools 标记解析失败(可观测)")
@@ -20,7 +20,6 @@ from foxclaw.core.model_router import ModelRouter
20
20
  from foxclaw.agents.base import CognitiveAgent
21
21
 
22
22
 
23
- # ── 1. P1 运行时注入 ───────────────────────────────────────────────────────────
24
23
  def test_runtime_context_and_idempotent():
25
24
  r = ModelRouter(provider="stub")
26
25
  ctx = r._runtime_context()
@@ -30,7 +29,6 @@ def test_runtime_context_and_idempotent():
30
29
  assert r._inject_runtime(a) == a, "二次注入必须幂等(防 stream→complete 重复)"
31
30
 
32
31
 
33
- # ── 2. ToolRegistry ────────────────────────────────────────────────────────────
34
32
  def test_registry_default_and_deny():
35
33
  reg = T.default_registry()
36
34
  assert set(reg.names()) == {"web_search", "recall_memory"}
@@ -55,7 +53,6 @@ def test_tool_use_flag_default_off():
55
53
  os.environ.pop("FOXCLAW_TOOL_USE", None)
56
54
 
57
55
 
58
- # ── 3. call_tools 降级 ─────────────────────────────────────────────────────────
59
56
  def test_call_tools_unsupported_degrades():
60
57
  r = ModelRouter(provider="stub")
61
58
  assert r.supports_tools() is False
@@ -63,7 +60,6 @@ def test_call_tools_unsupported_degrades():
63
60
  assert res["supported"] is False and res["tool_calls"] == []
64
61
 
65
62
 
66
- # ── 4. 工具循环 ────────────────────────────────────────────────────────────────
67
63
  class _FakeRouter:
68
64
  """脚本化 call_tools:按序返回预设结果,记录每轮 messages 长度。"""
69
65
  def __init__(self, script):
@@ -93,10 +89,8 @@ def _fake_agent(router):
93
89
 
94
90
  def test_reply_with_tools_multi_round():
95
91
  router = _FakeRouter([
96
- # 第1轮:模型要求调 web_search
97
92
  {"supported": True, "text": "", "assistant_msg": {"role": "assistant", "content": ""},
98
93
  "tool_calls": [{"id": "c1", "name": "web_search", "args": {"query": "最新模型"}}]},
99
- # 第2轮:拿到结果后给最终答案
100
94
  {"supported": True, "text": "根据搜索,最新是 X。", "tool_calls": [],
101
95
  "assistant_msg": {"role": "assistant", "content": "根据搜索,最新是 X。"}},
102
96
  ])
@@ -104,7 +98,6 @@ def test_reply_with_tools_multi_round():
104
98
  out = CognitiveAgent._reply_with_tools(agent, "有哪些新模型",
105
99
  [{"role": "user", "content": "有哪些新模型"}])
106
100
  assert out == "根据搜索,最新是 X。"
107
- # 第2轮的 messages 必须已追加 assistant(带 tool_calls)+ tool 结果
108
101
  _, round2_msgs = router.seen[1]
109
102
  roles = [m["role"] for m in round2_msgs]
110
103
  assert "assistant" in roles and "tool" in roles
@@ -129,7 +122,6 @@ def test_reply_with_tools_unsupported_returns_none():
129
122
 
130
123
 
131
124
  def test_reply_with_tools_empty_answer_returns_none():
132
- # 阶段五·步骤15:模型无工具调用且文本为空 → 返回 None 让上层 fallback(不再"空回复且不回退")
133
125
  router = _FakeRouter([
134
126
  {"supported": True, "text": "", "tool_calls": [],
135
127
  "assistant_msg": {"role": "assistant", "content": ""}},
@@ -139,7 +131,6 @@ def test_reply_with_tools_empty_answer_returns_none():
139
131
  assert out is None, "空答必须返回 None 让上层 fallback"
140
132
 
141
133
 
142
- # ── 5. 工具分派 ────────────────────────────────────────────────────────────────
143
134
  def test_dispatch_web_search_ok(monkeypatch):
144
135
  from foxclaw.core import web_search as ws
145
136
  monkeypatch.setattr(ws, "search_answer", lambda q: {"answer": "实时答案XYZ", "references": []})
@@ -168,11 +159,9 @@ def test_dispatch_denied(monkeypatch):
168
159
  assert "禁用" in out
169
160
 
170
161
 
171
- # ── 6. 动作级权限闸(permission 管线 + HITL)──────────────────────────────────────
172
162
  def test_tool_capability_readonly_and_failclosed():
173
163
  assert T.tool_capability("web_search")["read_only"] is True
174
164
  assert T.tool_capability("recall_memory")["side_effect"] == "read"
175
- # 未声明的新工具 → fail-closed:当有副作用 + 外部
176
165
  cap = T.tool_capability("some_future_write_tool")
177
166
  assert cap["read_only"] is False and cap["side_effect"] == "write" and cap["external"] is True
178
167
 
@@ -194,7 +183,6 @@ def test_dispatch_hitl_blocks_side_effect_tool(monkeypatch):
194
183
  called = {"n": 0}
195
184
  monkeypatch.setattr(ws, "search_answer",
196
185
  lambda q: called.__setitem__("n", called["n"] + 1) or {"answer": "x"})
197
- # 把 web_search 强制降级为有副作用 → 应被闸门拦成 HITL
198
186
  monkeypatch.setattr(T, "tool_capability",
199
187
  lambda name: {"side_effect": "send", "read_only": False,
200
188
  "external": True, "destructive": False})
@@ -219,7 +207,6 @@ def test_tool_path_injects_preferences(monkeypatch):
219
207
  fake.AGENT_ID = "assistant"
220
208
  fake._ability_augment = lambda t, m: m
221
209
  fake._skill_augment = lambda t, m: m
222
- # 偏好注入:把标记并进最后一条 user
223
210
  fake._pref_augment = lambda m: (list(m[:-1]) +
224
211
  [{"role": "user", "content": "【用户偏好】喜欢简洁\n\n" + m[-1]["content"]}])
225
212
 
@@ -70,7 +70,6 @@ def test_same_instance_concurrent(os_):
70
70
  per[sid] = per.get(sid, 0) + 1
71
71
  cross = sum(1 for _id, sid, p in rows for par in json.loads(p)
72
72
  if par in sid_of and sid_of[par] != sid)
73
- # goal 归属:每个返回会话的 goal 节点含本任务标记
74
73
  ok_goal = 0
75
74
  for i, r in enumerate(res):
76
75
  g = con.execute("SELECT content FROM thought_nodes WHERE session_id=? "
@@ -90,9 +89,9 @@ def test_supervisor_concurrent(os_):
90
89
 
91
90
  async def run():
92
91
  return await asyncio.gather(
93
- sup.run_task("分析本月理财预算"), # → finance
94
- sup.run_task("总结这篇论文的要点"), # → office(学习)
95
- sup.run_task("你能做什么"), # 元问题 → supervisor 自答
92
+ sup.run_task("分析本月理财预算"),
93
+ sup.run_task("总结这篇论文的要点"),
94
+ sup.run_task("你能做什么"),
96
95
  )
97
96
  res = asyncio.run(run())
98
97
  routed = [r["result"].get("dispatched_to") for r in res]
@@ -5,9 +5,6 @@ from pathlib import Path
5
5
  sys.path.insert(0, str(Path(__file__).parent.parent))
6
6
 
7
7
  def test_tracing():
8
- # 关掉 .env:否则开发者 .env 的 FOXCLAW_MCP_SERVER=1 会在 import api 时构建 MCP 服务器,
9
- # 而本测试有**两处** `with TestClient(app)`(82/189 行),第二次进 lifespan 会撞
10
- # StreamableHTTPSessionManager「.run() can only be called once per instance」而失败。
11
8
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
12
9
  os.environ["HKC_EMBEDDING"] = "stub"
13
10
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
@@ -20,27 +17,24 @@ def test_tracing():
20
17
 
21
18
  print("=" * 60); print("请求链路追踪验证"); print("=" * 60)
22
19
 
23
- # [1] 无 trace 时 span 是零成本 no-op(后台任务不该报错)
24
20
  clear_trace()
25
21
  assert current_trace() is None
26
22
  with span("orphan", x=1) as s:
27
23
  assert s is None
28
24
  print("[1] 无 trace 时 span no-op(不报错/不产生垃圾)OK")
29
25
 
30
- # [2] span 记录名称/耗时/meta;嵌套按结束顺序落盘
31
26
  tr = start_trace("unit")
32
27
  with span("outer", k="v"):
33
28
  with span("inner"):
34
29
  time.sleep(0.01)
35
30
  d = tr.to_dict()
36
31
  names = [s["name"] for s in d["spans"]]
37
- assert names == ["inner", "outer"], names # inner 先结束先记
32
+ assert names == ["inner", "outer"], names
38
33
  inner = d["spans"][0]
39
34
  assert inner["ok"] is True and inner["ms"] >= 9, inner
40
35
  assert d["spans"][1]["meta"] == {"k": "v"}
41
36
  print(f"[2] span 记录(嵌套/耗时 {inner['ms']}ms/meta)OK")
42
37
 
43
- # [3] 异常:记 ok=False + error,并原样抛出(不吞异常)
44
38
  try:
45
39
  with span("boom"):
46
40
  raise ValueError("kaboom")
@@ -51,14 +45,12 @@ def test_tracing():
51
45
  assert bad["ok"] is False and "kaboom" in bad["error"], bad
52
46
  print("[3] span 异常记录 ok=False + 原样抛出 OK")
53
47
 
54
- # [4] 单 trace span 上限(防病态循环撑爆内存)
55
48
  t2 = Trace("cap")
56
49
  for i in range(MAX_SPANS_PER_TRACE + 50):
57
50
  t2.add_span({"name": f"s{i}"})
58
51
  assert len(t2.to_dict()["spans"]) == MAX_SPANS_PER_TRACE and t2.to_dict()["dropped_spans"] == 50
59
52
  print(f"[4] 单 trace span 封顶 {MAX_SPANS_PER_TRACE}(超出计 dropped)OK")
60
53
 
61
- # [5] TraceStore 有界环存 + 按 id 取
62
54
  st = TraceStore(limit=3)
63
55
  ids = []
64
56
  for i in range(5):
@@ -67,7 +59,6 @@ def test_tracing():
67
59
  assert st.get(ids[-1]) is not None and st.get(ids[0]) is None, "应只留最近 3 条"
68
60
  print("[5] TraceStore 有界环存(只留最近 N)+ 按 id 取 OK")
69
61
 
70
- # [6] 线程隔离:contextvars 各线程独立,span 不串 trace
71
62
  clear_trace()
72
63
  res = {}
73
64
  def worker(name):
@@ -81,13 +72,10 @@ def test_tracing():
81
72
  assert all(v[1] == ["work"] for v in res.values()), "各线程 span 应各归各的 trace"
82
73
  print("[6] 线程隔离(3 线程 3 条独立 trace,span 不串)OK")
83
74
 
84
- # ── 端到端:经 API 请求产生 trace ─────────────────────────
85
75
  from fastapi.testclient import TestClient
86
76
  from foxclaw.api import app
87
77
 
88
78
  with TestClient(app) as c:
89
- # [7] 数据面请求 → 带 X-Trace-Id + 可查详情;链路里能看到 LLM span
90
- # (用 office:主管走路由管线,不经过 act/verify,埋点覆盖要用领域 Agent 验)
91
79
  r = c.post("/api/task", json={"task": "追踪自测", "agent_id": "office"})
92
80
  assert r.status_code == 200, r.status_code
93
81
  tid = r.headers.get("X-Trace-Id")
@@ -97,12 +85,9 @@ def test_tracing():
97
85
  names = [s["name"] for s in detail["spans"]]
98
86
  llm = [n for n in names if n.startswith("llm.")]
99
87
  assert llm, f"链路里应有 LLM span: {names}"
100
- # 跨层传播:Agent 跑在 asyncio.to_thread 的线程里,它的 span 仍落进本请求的 trace
101
- # → 证明 contextvars 确实穿过了线程边界(本设计的核心假设)
102
88
  mem = [n for n in names if n.startswith("memory.")]
103
89
  assert mem, f"跨线程的 Agent 记忆 span 应归属本 trace(contextvars 传播): {names}"
104
90
  assert all(s["ms"] >= 0 for s in detail["spans"]), "每个 span 都应有耗时"
105
- # 覆盖度:认知管线各 stage 都要在链路里(否则"慢在哪"会落在没埋点的暗区)
106
91
  stages = [n for n in names if n.startswith("stage.")]
107
92
  for must in ("stage.recall", "stage.act", "stage.verify"):
108
93
  assert must in names, f"缺 {must},埋点覆盖退化了: {names}"
@@ -110,21 +95,18 @@ def test_tracing():
110
95
  print(f"[7] 请求→trace + X-Trace-Id;{detail['span_count']} span "
111
96
  f"(管线 {len(stages)} stage + 跨线程 {mem[0]})OK")
112
97
 
113
- # [8] 列表可查;未知 id → 404
114
98
  lst = c.get("/api/traces?limit=5").json()["traces"]
115
99
  assert any(t["trace_id"] == tid for t in lst), "最近列表应含刚才那条"
116
100
  assert "spans" not in lst[0], "列表是摘要,不带 spans(省负载)"
117
101
  assert c.get("/api/traces/deadbeef0000").status_code == 404
118
102
  print(f"[8] /api/traces 列表({len(lst)} 条摘要)+ 未知 id 404 OK")
119
103
 
120
- # [9] 探针/静态不追踪(不污染)
121
104
  before = len(c.get("/api/traces?limit=200").json()["traces"])
122
105
  c.get("/health"); c.get("/metrics")
123
106
  after = c.get("/api/traces?limit=200").json()["traces"]
124
107
  assert not any(t["name"].endswith("/health") for t in after), "探针不该被追踪"
125
108
  print("[9] 探针 /health、/metrics 不进 trace OK")
126
109
 
127
- # ── 持久化 + 采样(内存环重启即失;慢的/出错的必须留得住)──────────
128
110
  import time as _t
129
111
  store = TraceStore(limit=2)
130
112
  store.attach_db(Path(os.environ["FOXCLAW_DATA_DIR"]) / "t.db")
@@ -134,7 +116,6 @@ def test_tracing():
134
116
  tr.duration_ms = ms; tr.start_ts = _t.time()
135
117
  return tr
136
118
 
137
- # [10] 采样:慢的必存、出错的必存、正常的(采样率 0)不存
138
119
  os.environ["FOXCLAW_TRACE_SLOW_MS"] = "500"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
139
120
  slow = _mk("GET /slow", 900); fast = _mk("GET /fast", 5); bad = _mk("GET /err", 5, err=True)
140
121
  for t in (slow, fast, bad):
@@ -145,21 +126,18 @@ def test_tracing():
145
126
  assert fast.trace_id not in ids, "正常请求(采样率0)不该落库"
146
127
  print("[10] 采样策略(慢/出错必存、正常按采样率不存)OK")
147
128
 
148
- # [11] 内存环滚出后,仍能从库里捞回慢请求 —— 这才是"查昨天那次慢请求"
149
- for i in range(5): # 环 limit=2,把 slow 挤出内存
129
+ for i in range(5):
150
130
  store.put(_mk(f"GET /x{i}", 1))
151
131
  assert store.get(slow.trace_id) is None, "应已滚出内存环"
152
132
  d = store.get_dict(slow.trace_id)
153
133
  assert d and d.get("persisted") and d["duration_ms"] == 900 and d["spans"], d
154
134
  print("[11] 内存环滚出后仍可从库捞回(重启/滚动后可查历史)OK")
155
135
 
156
- # [12] 过滤:按慢阈值 / 只看出错
157
136
  assert all(r["duration_ms"] >= 500 for r in store.query(limit=99, slow_ms=500))
158
137
  errs = store.query(limit=99, errors_only=True)
159
138
  assert errs and all(r["ok"] is False for r in errs), errs
160
139
  print("[12] 历史查询过滤(slow_ms / errors_only)OK")
161
140
 
162
- # [13] 保留上限:超出删最旧(磁盘有界)
163
141
  os.environ["FOXCLAW_TRACE_RETAIN"] = "100"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "1"
164
142
  for i in range(140):
165
143
  store.put(_mk(f"GET /bulk{i}", 1))
@@ -167,17 +145,14 @@ def test_tracing():
167
145
  os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
168
146
  print("[13] 落库保留上限(超出删最旧,磁盘有界)OK")
169
147
 
170
- # [14] 库不可用 → 优雅降级为仅内存(不能因为落库失败就把请求搞挂)
171
148
  s2 = TraceStore(limit=2)
172
149
  s2.attach_db("/nonexistent-dir-xyz/なし/t.db")
173
- s2.put(_mk("GET /ok", 900)) # 不应抛异常
150
+ s2.put(_mk("GET /ok", 900))
174
151
  assert s2.query(limit=9) == [] and len(s2.list(limit=9)) == 1
175
152
  print("[14] 库不可用 → 降级仅内存,不影响请求 OK")
176
153
 
177
- # ── W3C Trace Context:零依赖跨进程传播(Foxclaw 唯一的跨进程边界=MCP 双向)──
178
154
  from foxclaw.core.tracing import parse_traceparent, to_traceparent, current_traceparent
179
155
 
180
- # [15] traceparent 解析:合法接受,非法/全零一律拒(不信外部输入)
181
156
  good = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
182
157
  assert parse_traceparent(good) == "4bf92f3577b34da6a3ce929d0e0e4736"
183
158
  assert parse_traceparent("00-" + "0" * 32 + "-00f067aa0ba902b7-01") is None, "全零 trace-id 非法"
@@ -185,25 +160,21 @@ def test_tracing():
185
160
  assert parse_traceparent(bad_tp) is None, bad_tp
186
161
  print("[15] traceparent 解析(合法接受/非法+全零拒绝)OK")
187
162
 
188
- # [16] trace_id 符合 W3C 规格(32 hex)→ 出站 traceparent 可被对端接受
189
163
  t16 = Trace("w3c")
190
164
  assert len(t16.trace_id) == 32 and all(c in "0123456789abcdef" for c in t16.trace_id), t16.trace_id
191
165
  tp = to_traceparent(t16.trace_id)
192
166
  assert parse_traceparent(tp) == t16.trace_id, "自己生成的应能被自己解析(自洽)"
193
167
  print("[16] trace_id 32hex 合规 + 出站 traceparent 自洽 OK")
194
168
 
195
- # [17] 入站接链:带 traceparent 的请求应**复用**其 trace_id(两边链路对齐)
196
169
  with TestClient(app) as c:
197
170
  r = c.post("/api/task", json={"task": "接链自测", "agent_id": "supervisor"},
198
171
  headers={"traceparent": good})
199
172
  assert r.headers.get("X-Trace-Id") == "4bf92f3577b34da6a3ce929d0e0e4736", r.headers.get("X-Trace-Id")
200
- # 非法 traceparent → 不采信,自开一条
201
173
  r2 = c.post("/api/task", json={"task": "非法头", "agent_id": "supervisor"},
202
174
  headers={"traceparent": "garbage"})
203
175
  assert r2.headers.get("X-Trace-Id") and r2.headers["X-Trace-Id"] != "garbage"
204
176
  print("[17] 入站接链(合法 traceparent 复用其 trace_id;非法则自开)OK")
205
177
 
206
- # [18] 出站:current_traceparent 在 trace 内可用、trace 外为 None(MCP 调用据此带头)
207
178
  clear_trace()
208
179
  assert current_traceparent() is None
209
180
  t18 = start_trace("out")
@@ -211,17 +182,14 @@ def test_tracing():
211
182
  clear_trace()
212
183
  print("[18] 出站 current_traceparent(trace 内可用/trace 外 None)OK")
213
184
 
214
- # ── OTLP/HTTP-JSON 导出(可选、零依赖、绝不阻塞请求)───────────────
215
185
  from foxclaw.core.tracing import OTLP, _trace_to_otlp
216
186
 
217
- # [19] 默认关:未配 endpoint → 完全不启用(零成本)
218
187
  os.environ.pop("FOXCLAW_OTLP_ENDPOINT", None)
219
188
  assert OTLP.stats()["enabled"] is False
220
- OTLP.submit(_mk("GET /x", 5)) # 未配 endpoint → no-op,不该起线程/不该报错
189
+ OTLP.submit(_mk("GET /x", 5))
221
190
  print("[19] 未配 endpoint → OTLP 完全不启用(零成本)OK")
222
191
 
223
- # [20] OTLP/JSON 结构:根 span + 子 span(parentSpanId 挂根)、traceId 32hex、状态码
224
- t20 = start_trace("GET /demo") # 建 trace 并绑到当前上下文
192
+ t20 = start_trace("GET /demo")
225
193
  with span("child.ok"): pass
226
194
  try:
227
195
  with span("child.bad"):
@@ -244,7 +212,6 @@ def test_tracing():
244
212
  assert svc["key"] == "service.name" and svc["value"]["stringValue"] == "foxclaw"
245
213
  print(f"[20] OTLP/JSON 结构(根+{len(kids)} 子 span·parentSpanId·32hex traceId·状态码)OK")
246
214
 
247
- # [21] 端到端:起一个本地 collector,确认真的 POST /v1/traces 且 body 可解析
248
215
  import http.server, socketserver, threading as _th2
249
216
  received = []
250
217
  class _H(http.server.BaseHTTPRequestHandler):
@@ -259,7 +226,7 @@ def test_tracing():
259
226
  os.environ["FOXCLAW_OTLP_ENDPOINT"] = f"http://127.0.0.1:{port}"
260
227
  t21 = _mk("GET /exported", 12)
261
228
  OTLP.submit(t21)
262
- for _ in range(50): # 后台线程异步发,等它落地
229
+ for _ in range(50):
263
230
  if received: break
264
231
  _t.sleep(0.1)
265
232
  srv.shutdown()
@@ -270,8 +237,7 @@ def test_tracing():
270
237
  assert got["traceId"] == t21.trace_id and got["name"] == "GET /exported", got
271
238
  print("[21] 端到端导出(本地 collector 收到 POST /v1/traces,traceId 对得上)OK")
272
239
 
273
- # [22] 队列满 → 丢弃计数增长,submit 不阻塞(遥测绝不能拖慢请求)
274
- os.environ["FOXCLAW_OTLP_ENDPOINT"] = "http://127.0.0.1:1" # 打不通 → worker 卡住,队列会满
240
+ os.environ["FOXCLAW_OTLP_ENDPOINT"] = "http://127.0.0.1:1"
275
241
  from foxclaw.core.tracing import _OtlpExporter
276
242
  ex = _OtlpExporter(maxsize=2)
277
243
  before = ex.dropped
@@ -72,13 +72,12 @@ def test_pending_bounded_no_leak():
72
72
  print("\n【3b】深审回归:持续结晶下 _pending 不无界增长(prune 已完成 Future)")
73
73
  hkc = _HKC()
74
74
  tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=True)
75
- for i in range(200): # 模拟长期运行:大量 truth.appended
75
+ for i in range(200):
76
76
  tc.on_truth(_msg({"truth": {"fact": f"事实{i}"}, "agent_id": "office"}))
77
- tc.drain() # 等全部结晶完成
78
- # 再来一发触发 prune,此时之前 200 个都已 done → 被清掉
77
+ tc.drain()
79
78
  tc.on_truth(_msg({"truth": {"fact": "末尾"}, "agent_id": "office"}))
80
79
  import time as _t
81
- for _ in range(50): # 等末尾这发也完成
80
+ for _ in range(50):
82
81
  if all(f.done() for f in tc._pending):
83
82
  break
84
83
  _t.sleep(0.02)
@@ -95,9 +94,8 @@ def test_integration_wiring():
95
94
  from foxclaw import Foxclaw
96
95
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
97
96
  fake = _HKC()
98
- os_.truth_crystallizer._hkc_provider = lambda: fake # 换成假 HKC(不调 LLM)
97
+ os_.truth_crystallizer._hkc_provider = lambda: fake
99
98
  os_.truth_crystallizer.enabled = True
100
- # 真值链末端事件 → 结晶器应被 Foxclaw 订阅到并触发
101
99
  os_.system_bus.publish("truth.appended",
102
100
  {"truth": {"fact": "测试真值事实"}, "agent_id": "finance"})
103
101
  os_.truth_crystallizer.drain()
@@ -15,7 +15,7 @@ import os, sys, asyncio, tempfile, shutil
15
15
  from pathlib import Path
16
16
  os.environ.setdefault("HF_HUB_OFFLINE", "1")
17
17
  os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
18
- os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast" # harness 走 hash,快
18
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
19
19
  sys.path.insert(0, str(Path(__file__).parent.parent))
20
20
 
21
21
 
@@ -30,13 +30,11 @@ def main():
30
30
  seen = []
31
31
  os_.event_spine.project(lambda e: seen.append(e.topic))
32
32
 
33
- # 1. _no_emit 守卫:子 Agent 不自发
34
33
  print("\n【1】_no_emit 守卫:分发时子 Agent 不自发 execution.completed")
35
34
  asyncio.run(os_.finance.run_task("守卫测试", {"_no_emit": True}))
36
35
  assert "execution.completed" not in seen, "带 _no_emit 时不应发 execution.completed"
37
36
  print(" ✓ context['_no_emit']=True → 子 Agent 未自发(留给主管统一发)")
38
37
 
39
- # 2. 全路径触发:直连任务发恰好一条
40
38
  print("\n【2】全路径触发:直连领域 Agent 任务发恰好一条 execution.completed")
41
39
  before = seen.count("execution.completed")
42
40
  asyncio.run(os_.finance.run_task("分析我的月度开支"))
@@ -44,7 +42,6 @@ def main():
44
42
  assert emitted == 1, f"直连任务应发恰好 1 条,实得 {emitted}"
45
43
  print(f" ✓ 直连 finance.run_task → execution.completed ×{emitted}")
46
44
 
47
- # 3. 共享真值记录:别的 Agent 能召回
48
45
  print("\n【3】共享真值记录:直连任务经 TruthPipeline 产生 shared 记录,跨 Agent 可见")
49
46
  shared = os_.hmr.recall(query="分析我的月度开支 处理", agent_id="office", top_k=10)
50
47
  shared_hits = [m for m in shared if m.get("shared")]
@@ -52,7 +49,6 @@ def main():
52
49
  print(f" ✓ office 召回到 {len(shared_hits)} 条共享真值记录(来自 finance 的直连任务)")
53
50
  print(f" 样例: {shared_hits[0].get('content','')[:50]!r}")
54
51
 
55
- # 4. 被拦截的直连任务:发 blocked=True、不落共享真值记录、CognitiveState 不崩
56
52
  print("\n【4】被拦截的直连任务:blocked=True + 不落共享记录 + 观测不崩")
57
53
  import types
58
54
  from foxclaw.agents.base import HarnessBlockedError
@@ -68,11 +64,10 @@ def main():
68
64
  b_emitted = [x for x in blocked_flags if x is True]
69
65
  assert r.get("error") == "harness_blocked"
70
66
  assert len(b_emitted) - b_before == 1, "被拦截直连应发恰好一条 blocked=True"
71
- # 精确判定:blocked 任务本身没产生真值记录(按内容过滤;语义召回可能带回 check2 的旧记录)
72
67
  own = [m for m in os_.hmr.recall(query="危险越界任务 处理", agent_id="office", top_k=10)
73
68
  if m.get("shared") and "危险越界" in m.get("content", "")]
74
69
  assert len(own) == 0, "被拦截任务不应产生自己的共享真值记录(TruthPipeline 跳过 blocked)"
75
- snap = os_.cognitive_state.snapshot() # 缺 schedule 的直连事件不应让观测崩溃
70
+ snap = os_.cognitive_state.snapshot()
76
71
  assert "active_agent" in snap
77
72
  print(f" ✓ blocked=True ×1、无共享记录、CognitiveState 快照正常(focus={snap.get('semantic_focus')})")
78
73