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
@@ -7,7 +7,6 @@ import os, sys, tempfile, shutil
7
7
  from pathlib import Path
8
8
  sys.path.insert(0, str(Path(__file__).parent.parent))
9
9
 
10
- # ── 环境隔离:必须在 import foxclaw 之前(import 会读这些键初始化) ─────────────
11
10
  for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
12
11
  "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
13
12
  os.environ.pop(_k, None)
@@ -15,10 +14,10 @@ os.environ["HKC_EMBEDDING"] = "stub"
15
14
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
16
15
  os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
17
16
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
18
- os.environ["FOXCLAW_PROVIDER"] = "" # 不指定 provider → 无 key 时降级 stub(确定性)
17
+ os.environ["FOXCLAW_PROVIDER"] = ""
19
18
  os.environ["FOXCLAW_MODEL"] = ""
20
19
  os.environ["FOXCLAW_MCP_SERVER"] = "0"
21
- os.environ["HF_HUB_OFFLINE"] = "1" # 离线:HMR embedding 读本地缓存,不联网
20
+ os.environ["HF_HUB_OFFLINE"] = "1"
22
21
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
23
22
 
24
23
  import pytest
@@ -40,31 +39,25 @@ def store():
40
39
 
41
40
 
42
41
  def test_sanitize_attachments():
43
- # 数量上限
44
42
  many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
45
43
  assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
46
44
 
47
- # 单文件正文截断到上限
48
45
  one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
49
46
  assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
50
47
 
51
- # name 截断到 200
52
48
  assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
53
49
 
54
- # 累计正文封顶:单文件先截到 20000,4×20000 累计 60000 封顶 → 第 4 个正文被清空(名字仍在)
55
50
  big = "x" * (_ATTACH_TEXT_MAX + 1000)
56
51
  four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
57
52
  assert len(four) == 4
58
53
  assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
59
54
  assert four[3]["text"] == ""
60
55
 
61
- # 非列表 / 非 dict / 空 name+空 text → 全部剔除
62
56
  assert _sanitize_attachments("notalist") == []
63
57
  assert _sanitize_attachments([1, "x"]) == []
64
58
  assert _sanitize_attachments([{"name": "", "text": ""}]) == []
65
59
  assert _sanitize_attachments(None) == []
66
60
 
67
- # 字段类型清洗:size 非数字 → 0;mime/url 转 str
68
61
  out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
69
62
  assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
70
63
  print("护栏(数量/单文件/累计/类型清洗/边界)OK")
@@ -78,10 +71,10 @@ def test_attachments_context():
78
71
  ctx = CognitiveAgent._attachments_context(atts)
79
72
  assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
80
73
  assert "【附件「b.txt」" not in ctx
81
- assert "b.pdf" in ctx and "无法提取" in ctx # 非文本降级为「文件名 + 元数据」提示
74
+ assert "b.pdf" in ctx and "无法提取" in ctx
82
75
  assert CognitiveAgent._attachments_context([]) == ""
83
76
  assert CognitiveAgent._attachments_context(None) == ""
84
- assert CognitiveAgent._attachments_context([1, "x"]) == "" # 非 dict 全跳过
77
+ assert CognitiveAgent._attachments_context([1, "x"]) == ""
85
78
  print("LLM 上下文注入(正文/降级/空)OK")
86
79
 
87
80
 
@@ -98,7 +91,6 @@ def test_sessions_append_attachments(store):
98
91
  assert len(m["attachments"]) == 2
99
92
  assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
100
93
  assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
101
- # 不带附件 → 无 attachments 字段(向后兼容)
102
94
  assert store.append(s["id"], "assistant", "好")
103
95
  assert "attachments" not in store.get(s["id"])["messages"][1]
104
96
  print("会话落盘(轻字段/不存 text/向后兼容)OK")
@@ -106,42 +98,35 @@ def test_sessions_append_attachments(store):
106
98
 
107
99
  def test_upload_and_download():
108
100
  with TestClient(app) as c:
109
- # 上传文本文件 → 提取正文
110
101
  r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
111
102
  assert r.status_code == 200, r.text
112
103
  a = r.json()
113
104
  assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
114
105
  assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
115
106
 
116
- # 下载 → 同一内容
117
107
  r2 = c.get(a["url"])
118
108
  assert r2.status_code == 200
119
109
  assert r2.content.decode("utf-8") == "今天学了三件事"
120
110
 
121
- # 二进制(不可提取)→ text 为空,降级
122
111
  r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
123
112
  assert r3.status_code == 200 and r3.json()["text"] == ""
124
113
 
125
- # 空文件 → 400;超大 → 413
126
114
  assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
127
115
  big = b"x" * (21 * 1024 * 1024)
128
116
  assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
129
117
 
130
- # 路径穿越:HTTP 层规范化后到不了附件路由(404);非法 id 字符 → 400;合法但不存在 → 404
131
- assert c.get("/api/attachments/../../etc/passwd").status_code == 404 # .. 被 URL 规范化,不匹配路由
132
- assert c.get("/api/attachments/a.b").status_code == 400 # . 不在 id 白名单 [_A-Za-z0-9_-]
133
- assert c.get("/api/attachments/deadbeef0000").status_code == 404 # 合法 id 但文件不存在
118
+ assert c.get("/api/attachments/../../etc/passwd").status_code == 404
119
+ assert c.get("/api/attachments/a.b").status_code == 400
120
+ assert c.get("/api/attachments/deadbeef0000").status_code == 404
134
121
  print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
135
122
 
136
123
 
137
124
  def test_chat_stream_with_attachments():
138
125
  import foxclaw.api as _api
139
126
  with TestClient(app) as c:
140
- # 先上传一个带正文的附件,拿到引用
141
127
  up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
142
128
  att = up.json()
143
- # 会话须先存在,append 才会落盘(前端 ensureSession 同款:POST /api/sessions → 拿 id)
144
- sid = _api._os_instance.sessions.create()["id"]
129
+ sid = _api.os_instance().sessions.create()["id"]
145
130
  payload = {
146
131
  "task": "帮我总结要点",
147
132
  "agent_id": "supervisor",
@@ -153,8 +138,7 @@ def test_chat_stream_with_attachments():
153
138
  assert r.status_code == 200, r.text[:200]
154
139
  assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
155
140
 
156
- # 会话已落盘用户消息 + 附件元数据(不含 text)
157
- d = _api._os_instance.sessions.get(sid)
141
+ d = _api.os_instance().sessions.get(sid)
158
142
  assert d is not None, "会话未落盘"
159
143
  um = d["messages"][0]
160
144
  assert um["role"] == "user" and um["content"] == "帮我总结要点"
@@ -24,8 +24,6 @@ def _write_chain(path: Path, records) -> None:
24
24
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
25
25
 
26
26
 
27
- # ── 链 ─────────────────────────────────────────────────────────────
28
-
29
27
  def test_chain_passes(tmp_path):
30
28
  from foxclaw.core import audit_chain
31
29
 
@@ -42,7 +40,7 @@ def test_tampered_line_detected(tmp_path):
42
40
  _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
43
41
  lines = p.read_text(encoding="utf-8").splitlines()
44
42
  rec = json.loads(lines[0])
45
- rec["verdict"] = "allow" # 把一次拒绝抹成放行
43
+ rec["verdict"] = "allow"
46
44
  lines[0] = json.dumps(rec, ensure_ascii=False)
47
45
  p.write_text("\n".join(lines) + "\n", encoding="utf-8")
48
46
 
@@ -69,7 +67,7 @@ def test_resume_after_restart(tmp_path):
69
67
  p = tmp_path / "governance.jsonl"
70
68
  GovernanceAuditor(sink_path=p).record(
71
69
  layer="skill", actor="office", action="write_note", verdict="success")
72
- aud = GovernanceAuditor(sink_path=p) # 模拟重启
70
+ aud = GovernanceAuditor(sink_path=p)
73
71
  aud.record(layer="skill", actor="office", action="write_note", verdict="success")
74
72
 
75
73
  ok, n, msg = aud.verify()
@@ -96,8 +94,6 @@ def test_tail_hash_ignores_torn_line(tmp_path):
96
94
  assert audit_chain.tail_hash(p) == "h1"
97
95
 
98
96
 
99
- # ── recovery 脱敏 ──────────────────────────────────────────────────
100
-
101
97
  def test_recovery_step_args_and_result_redacted():
102
98
  from types import SimpleNamespace
103
99
 
@@ -117,5 +113,4 @@ def test_recovery_step_args_and_result_redacted():
117
113
 
118
114
  assert "sk-" not in json.dumps(got["args"]), got["args"]
119
115
  assert "[已脱敏]" in got["result"], got["result"]
120
- # key 必须用未脱敏的 args 算,否则同一步骤前后 key 不一致(缓存/回放对不上)
121
116
  assert got["key"] == _step_key("http", {"api_key": key})
@@ -42,7 +42,6 @@ def test_unit():
42
42
  "reason": "office 的职责域不含 ['type:decision','finance.transfer']",
43
43
  "mode": "audit", "content_sha": "0123456789abcdef",
44
44
  "content_preview": "财务应给办公转账1000元"}, source="office")
45
- # 非本主题的事件不应落盘(subscribe 前缀精确命中)
46
45
  spine.publish("memory.updated", {"x": 1})
47
46
 
48
47
  check(path.exists(), "审计文件已创建(含 audit/ 目录)")
@@ -55,7 +54,6 @@ def test_unit():
55
54
  check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
56
55
  check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
57
56
 
58
- # 追加语义:第二条接在后面,顺序保持
59
57
  spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
60
58
  "tags": ["truth_pipeline"], "allowed": True,
61
59
  "reason": "命中全局允许 facet", "mode": "audit",
@@ -65,12 +63,10 @@ def test_unit():
65
63
  check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
66
64
  "追加写不覆盖旧行,顺序保持")
67
65
 
68
- # 坏行容错:夹一行垃圾,read() 跳过而不是崩
69
66
  with open(path, "a", encoding="utf-8") as f:
70
67
  f.write("{这不是JSON}\n")
71
68
  check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
72
69
 
73
- # 开关:环境变量置 0 时 attach 不挂
74
70
  old = os.environ.get(ENV_SWITCH)
75
71
  os.environ[ENV_SWITCH] = "0"
76
72
  try:
@@ -99,7 +95,6 @@ def test_end_to_end():
99
95
  audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
100
96
  check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
101
97
 
102
- # 一条合法 + 一条越界(audit 模式不拦截,只记录)——真实闸门口径
103
98
  osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
104
99
  tags=["preference"], shared=True)
105
100
  osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
@@ -112,7 +107,6 @@ def test_end_to_end():
112
107
  denied = [r for r in recs if not r["payload"]["allowed"]]
113
108
  check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
114
109
  "越界那条可从文件直接判读")
115
- # 与脊柱 history 口径一致:文件条数 == 脊柱里该主题条数
116
110
  spine_n = len(osi.event_spine.history("memory.shared.write", 100))
117
111
  check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
118
112
  finally:
@@ -0,0 +1,198 @@
1
+ """
2
+ 行为规范(Behavior Spec)测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. 内置 behavior.yaml 可加载,id 唯一,每个 code_check 都解析到真实可调用对象。
6
+ 2. 检查点不是装饰:policy_gate.check_action / confirms.consume / verify.verify 在业务代码里真有调用。
7
+ 3. prompt 注入:行为规范在最前段(Markdown 内核之前、persona 之前),带 spec_version,可整体关闭。
8
+ 4. 本地覆盖:可新增 / 可收紧;放宽或省略删除严格层规则 → BehaviorLoadError。
9
+ 5. 本地覆盖的语法错、版本过期 → 降级为 warning 并忽略,不把应用锁死(内置文件的问题仍然硬失败)。
10
+ """
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
18
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
19
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
20
+ sys.path.insert(0, str(Path(__file__).parent.parent))
21
+
22
+ from foxclaw.agents.mixins.prompt import PromptMixin
23
+ from foxclaw.behavior import BehaviorLoadError, load, local_path, prompt_block
24
+
25
+ ROOT = Path(__file__).parent.parent
26
+
27
+ BASE_YAML = """
28
+ version: 1
29
+ spec_id: test-behavior
30
+ prohibitions:
31
+ - id: no_x
32
+ rule: 不做 X
33
+ enforce: both
34
+ code_check: "foxclaw.core.verify:verify"
35
+ confirmations:
36
+ - id: confirm_y
37
+ rule: 做 Y 前先确认
38
+ enforce: prompt
39
+ uncertainty:
40
+ - id: zzz
41
+ rule: 不确定就问
42
+ enforce: prompt
43
+ """
44
+
45
+
46
+ def _write(tmp_path, name, text):
47
+ p = tmp_path / name
48
+ p.write_text(text, encoding="utf-8")
49
+ return p
50
+
51
+
52
+ def _loaded(tmp_path, local_text):
53
+ return load(_write(tmp_path, "behavior.yaml", BASE_YAML),
54
+ _write(tmp_path, "behavior.local.yaml", local_text))
55
+
56
+
57
+ def test_builtin_spec_loads_with_unique_ids():
58
+ spec = load()
59
+ ids = [r.id for r in spec.rules]
60
+ assert len(ids) == len(set(ids))
61
+ assert spec.version == 1 and spec.spec_version
62
+ assert {"confirm_side_effect", "no_silent_failure"} <= set(ids)
63
+
64
+
65
+ def test_every_code_check_resolves_to_callable():
66
+ spec = load()
67
+ checks = spec.checkpoints()
68
+ assert checks, "内置规范至少要登记一个真实检查点"
69
+ assert all(callable(fn) for fn in checks.values())
70
+
71
+
72
+ def test_checkpoints_are_really_wired_in_business_code():
73
+ """反漂移:登记了检查点,业务代码里就必须真有调用,否则清单会退化成装饰。"""
74
+ blob = "\n".join(p.read_text(encoding="utf-8")
75
+ for p in (ROOT / "foxclaw").rglob("*.py")
76
+ if p.parts[len(ROOT.parts)] != "behavior")
77
+ assert "policy_gate.check_action(" in blob
78
+ assert "confirms.consume(" in blob
79
+ assert "verify.verify(" in blob
80
+
81
+
82
+ def test_prompt_block_leads_with_spec_and_version():
83
+ spec = load()
84
+ block = prompt_block(spec)
85
+ assert block.startswith("[行为规范")
86
+ assert spec.spec_version in block
87
+ for rule in spec.injectable():
88
+ assert f"[{rule.id}]" in block
89
+ assert "不可协商" in block
90
+
91
+
92
+ def test_behavior_precedes_kernel_and_role():
93
+ import types
94
+ fake = types.SimpleNamespace(_role_prompt="我是角色ROLEXYZ。", AGENT_ID="finance")
95
+ out = PromptMixin.system_prompt.fget(fake)
96
+ assert out.index("[行为规范") < out.index("认知操作系统") < out.index("ROLEXYZ")
97
+
98
+
99
+ def test_switch_disables_behavior_block(monkeypatch):
100
+ monkeypatch.setenv("FOXCLAW_BEHAVIOR", "0")
101
+ assert prompt_block() == ""
102
+
103
+
104
+ def test_local_can_add_and_tighten(tmp_path):
105
+ spec = _loaded(tmp_path, """
106
+ version: 1
107
+ confirmations:
108
+ - id: confirm_y
109
+ rule: 做 Y 前先确认(收紧)
110
+ enforce: both
111
+ code_check: "foxclaw.core.verify:verify"
112
+ uncertainty:
113
+ - id: new_rule
114
+ rule: 新加的更严规则
115
+ enforce: prompt
116
+ """)
117
+ assert spec.by_id("confirm_y").enforce == "both"
118
+ assert spec.by_id("new_rule") is not None
119
+ assert spec.by_id("no_x").enforce == "both" # 未提及的保持不变
120
+
121
+
122
+ def test_local_cannot_weaken_enforce(tmp_path):
123
+ with pytest.raises(BehaviorLoadError, match="不能放宽"):
124
+ _loaded(tmp_path, """
125
+ prohibitions:
126
+ - id: no_x
127
+ rule: 不做 X
128
+ enforce: prompt
129
+ """)
130
+
131
+
132
+ def test_local_cannot_drop_strict_layer_rule(tmp_path):
133
+ with pytest.raises(BehaviorLoadError, match="不能通过省略删除"):
134
+ _loaded(tmp_path, "confirmations: []\n")
135
+
136
+
137
+ def test_local_syntax_error_is_warning_not_fatal(tmp_path):
138
+ spec = _loaded(tmp_path, "prohibitions: [oops\n")
139
+ assert spec.warnings and "已忽略" in spec.warnings[0]
140
+ assert spec.by_id("no_x") is not None
141
+
142
+
143
+ def test_local_stale_version_is_ignored(tmp_path):
144
+ spec = _loaded(tmp_path, "version: 99\nuncertainty: []\n")
145
+ assert any("已过期" in w for w in spec.warnings)
146
+ assert spec.by_id("zzz") is not None
147
+
148
+
149
+ def test_builtin_version_mismatch_is_fatal(tmp_path):
150
+ p = _write(tmp_path, "behavior.yaml", "version: 99\nprohibitions: []\n")
151
+ with pytest.raises(BehaviorLoadError, match="version"):
152
+ load(p, tmp_path / "none.yaml")
153
+
154
+
155
+ def test_enforce_without_code_check_fails(tmp_path):
156
+ p = _write(tmp_path, "behavior.yaml", """
157
+ version: 1
158
+ prohibitions:
159
+ - id: no_x
160
+ rule: 不做 X
161
+ enforce: both
162
+ """)
163
+ with pytest.raises(BehaviorLoadError, match="没有 code_check"):
164
+ load(p, tmp_path / "none.yaml")
165
+
166
+
167
+ def test_unknown_checkpoint_fails(tmp_path):
168
+ p = _write(tmp_path, "behavior.yaml", """
169
+ version: 1
170
+ prohibitions:
171
+ - id: no_x
172
+ rule: 不做 X
173
+ enforce: code
174
+ code_check: "foxclaw.core.verify:no_such_function"
175
+ """)
176
+ with pytest.raises(BehaviorLoadError, match="不存在"):
177
+ load(p, tmp_path / "none.yaml")
178
+
179
+
180
+ def test_duplicate_id_fails(tmp_path):
181
+ p = _write(tmp_path, "behavior.yaml", """
182
+ version: 1
183
+ prohibitions:
184
+ - id: no_x
185
+ rule: A
186
+ enforce: prompt
187
+ uncertainty:
188
+ - id: no_x
189
+ rule: B
190
+ enforce: prompt
191
+ """)
192
+ with pytest.raises(BehaviorLoadError, match="重复"):
193
+ load(p, tmp_path / "none.yaml")
194
+
195
+
196
+ def test_local_path_follows_data_dir(monkeypatch, tmp_path):
197
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
198
+ assert local_path() == tmp_path / "behavior.local.yaml"
@@ -91,10 +91,8 @@ async def test_service_wrapper():
91
91
  print("\n【3】BrowserService async 包装(注入假 session,经专用工作线程)")
92
92
  svc = BrowserService()
93
93
  fake = FakeSession()
94
- # 绕过 playwright:预置会话,则真实 _ensure_session_sync 因 _session 非 None 直接返回它,
95
- # 不构造 SessionManager/不 launch chromium。强制 available=True 走真实工作线程路径。
96
94
  svc._session = fake
97
- svc.available = lambda: True # 实例属性覆盖类 staticmethod
95
+ svc.available = lambda: True
98
96
 
99
97
  p = await svc.perceive()
100
98
  assert p["ok"] and p["semantic_state"]["title"] == "P1"
@@ -104,16 +102,14 @@ async def test_service_wrapper():
104
102
  assert r["ok"] and r["verified"]["success"]
105
103
  print(" ✓ act(navigate) 返回校验后结果 verified.success=True")
106
104
 
107
- bad = await svc.act({"target": "3"}) # 缺 type
105
+ bad = await svc.act({"target": "3"})
108
106
  assert bad["ok"] is False and "非法动作" in bad["error"]
109
107
  print(" ✓ 非法动作(缺 type)被结构化拒绝")
110
108
 
111
109
  await svc.close()
112
110
  assert fake.closed, "close() 必须在工作线程上释放会话"
113
111
  print(" ✓ close() 释放了会话")
114
- # 幂等:再次 close 不报错
115
112
  await svc.close()
116
- # 关停后调用被拒(executor 已关)
117
113
  after_close = await svc.perceive()
118
114
  assert after_close["ok"] is False
119
115
  print(" ✓ close() 幂等 + 关停后调用被安全拒绝")
@@ -10,8 +10,6 @@ from foxclaw.browser.action import url_policy
10
10
  from foxclaw.browser.driver.playwright_driver import PlaywrightDriver
11
11
 
12
12
 
13
- # ── 判定层 ────────────────────────────────────────────────────
14
-
15
13
  @pytest.mark.parametrize("url", [
16
14
  "data:image/png;base64,iVBORw0KGgo=",
17
15
  "blob:https://example.com/2f8c-4a",
@@ -112,8 +110,6 @@ def test_short_ipv6_with_scope_is_not_mistaken(monkeypatch):
112
110
  url_policy._resolve_cache.clear()
113
111
 
114
112
 
115
- # ── 路由回调 ──────────────────────────────────────────────────
116
-
117
113
  class _FakeRoute:
118
114
  def __init__(self, url):
119
115
  self.request = type("Req", (), {"url": url})()
@@ -43,10 +43,8 @@ def test_cycles_broken_structural():
43
43
  try:
44
44
  from foxclaw import Foxclaw
45
45
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
46
- # 环1:Agent 不再直接持 search_runtime
47
46
  assert not hasattr(os_.finance, "_search_runtime"), "Agent 不应有 _search_runtime 直接引用"
48
47
  assert os_.finance._caps is os_.capabilities, "Agent 改持能力中枢"
49
- # 环2:Orchestrator 不再把 agents 存为实例字段(改为经中枢解析的 property)
50
48
  assert "agents" not in os_.orchestrator.__dict__, "orchestrator 不应把 agents 存为实例字段"
51
49
  assert os_.orchestrator._caps is os_.capabilities
52
50
  assert os_.orchestrator.agents is os_._agents, "orchestrator.agents 经中枢解析"
@@ -63,17 +61,14 @@ def test_behavior_preserved():
63
61
  try:
64
62
  from foxclaw import Foxclaw
65
63
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
66
- # 跨 Agent 编排(orchestrator 经中枢解析 finance)
67
64
  r = os_.supervisor.cross_request(
68
65
  from_agent="admin", to_agent="finance", request_type="budget_request",
69
66
  payload={"amount": 150, "category": "travel", "entity": "高铁", "account": "default"},
70
67
  reason="出差")
71
68
  assert r["status"] in ("approved", "confirm_required", "rejected"), f"裁决异常 {r}"
72
69
  print(f" ✓ cross_request 经中枢解析 agents → 状态 {r['status']}")
73
- # Agent 反向调用内部搜索(经中枢解析 search_runtime)
74
70
  res = os_.finance.search_internal("测试查询")
75
71
  assert res is not None, "search_internal 应经中枢解析到 search_runtime"
76
- # 中枢缓存:两次解析同一实例
77
72
  assert os_.capabilities.get("search_runtime") is os_.search_runtime
78
73
  print(" ✓ search_internal 经中枢解析 search_runtime(缓存一致)")
79
74
  asyncio.run(os_.aclose())
@@ -101,7 +96,6 @@ def test_lazy_build_once_concurrent():
101
96
  for t in ts: t.join()
102
97
  assert calls["n"] == 1, f"并发下惰性应只构造 1 次,实得 {calls['n']}"
103
98
  assert len({id(r) for r in results}) == 1, "12 线程应拿到同一实例"
104
- # 不同能力不互相阻塞(构造锁 per-name;此处只验证正确性:各自构造一次)
105
99
  reg.register_lazy("y", lambda: object())
106
100
  assert reg.get("y") is reg.get("y")
107
101
  print(" ✓ 12 线程并发 get 同一惰性 → 只构造 1 次、同一实例;不同能力独立")
@@ -23,8 +23,6 @@ def _headers(secret: str = SECRET, body: bytes = b'{"a":1}', ts: float = 1_700_0
23
23
  return signer.headers(body, nonce="n0nce", now=ts)
24
24
 
25
25
 
26
- # ── canonical / 签名 ────────────────────────────────────────
27
-
28
26
  def test_canonical_format_is_locked() -> None:
29
27
  """canonical 是两端的协议:字段顺序/分隔符一改,对方全部验签失败。"""
30
28
  line = S.canonical("1700000000", "abc", b"body").decode()
@@ -63,7 +61,6 @@ def test_replay_guard_rejects_second_use() -> None:
63
61
  guard = S.ReplayGuard(ttl=300)
64
62
  assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
65
63
  assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (False, "replay")
66
- # 换一个 nonce(正常新请求)→ 通过
67
64
  h2 = S.OutboundSigner(SECRET).headers(body, nonce="other", now=1_700_000_000)
68
65
  assert S.verify(h2, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
69
66
 
@@ -82,12 +79,10 @@ def test_verify_reason_codes() -> None:
82
79
 
83
80
  def test_header_lookup_is_case_insensitive() -> None:
84
81
  body = b"{}"
85
- h = {k.lower(): v for k, v in _headers(body=body).items()} # ASGI 常给小写
82
+ h = {k.lower(): v for k, v in _headers(body=body).items()}
86
83
  assert S.verify(h, body, SECRET, now=1_700_000_000) == (True, "ok")
87
84
 
88
85
 
89
- # ── signer ─────────────────────────────────────────────────
90
-
91
86
  def test_signer_optional_key_id_and_disabled_state() -> None:
92
87
  h = S.OutboundSigner(SECRET, key_id="k9").headers(b"{}")
93
88
  assert h[S.KEY_ID_HEADER] == "k9"
@@ -105,8 +100,6 @@ def test_from_env_opt_in() -> None:
105
100
  assert S.from_env({S.ENV_SECRET: " "}) is None
106
101
 
107
102
 
108
- # ── 桥接层 ─────────────────────────────────────────────────
109
-
110
103
  class _Resp:
111
104
  status_code = 200
112
105
  text = ""
@@ -155,7 +148,7 @@ def test_bridge_signs_the_bytes_it_sends(fake_http) -> None:
155
148
  assert isinstance(body, bytes) and json.loads(body)["text"] == "hi"
156
149
  assert cap["headers"]["Content-Type"] == "application/json"
157
150
  assert cap["headers"]["Authorization"] == "Bearer t"
158
- ok, why = S.verify(cap["headers"], body, SECRET) # 网关侧照此验签
151
+ ok, why = S.verify(cap["headers"], body, SECRET)
159
152
  assert (ok, why) == (True, "ok")
160
153
 
161
154
 
@@ -23,7 +23,6 @@ def check(cond, msg):
23
23
  def test_abstraction():
24
24
  print("\n【1】渠道抽象层")
25
25
  from foxclaw.channels import ChannelAdapter, InboundMessage, SendResult, ChannelRegistry, OutboundMessage
26
- # handle_raw 异常隔离:on_message 抛异常不冒泡
27
26
  class A(ChannelAdapter):
28
27
  CHANNEL = "t"
29
28
  def normalize(self, raw):
@@ -38,7 +37,6 @@ def test_abstraction():
38
37
  for txt in ["a", "bad", "c"]:
39
38
  ad.handle_raw({"text": txt})
40
39
  check(processed == ["a", "c"], "handle_raw 异常隔离(单条失败不影响其他)")
41
- # 注册表
42
40
  reg = ChannelRegistry(); reg.register(ad)
43
41
  check(reg.get("t") is ad, "渠道注册表")
44
42
  check(not reg.send("nope", OutboundMessage("c", "x")).ok, "未注册渠道发送失败")
@@ -69,14 +67,12 @@ def test_weixin():
69
67
  check(h["Authorization"].startswith("Bearer "), "Bearer token")
70
68
  check(int(base64.b64decode(h["X-WECHAT-UIN"]).decode()) >= 0, "X-WECHAT-UIN 格式")
71
69
  check(wx._build_client_version("2.4.3") == 132099, "ClientVersion 计算")
72
- # 标准化 + context_token 记忆
73
70
  msg = {"message_id": 1, "from_user_id": "u1", "session_id": "s",
74
71
  "message_type": MSG_FROM_USER, "context_token": "ctx1",
75
72
  "item_list": [{"type": ITEM_TEXT, "text_item": {"text": "你好"}}]}
76
73
  inb = wx._normalize_msg(msg)
77
74
  check(inb.text == "你好", "消息标准化")
78
75
  check(wx._context_tokens.get("u1") == "ctx1", "记住 context_token")
79
- # bot 回显忽略
80
76
  check(wx._normalize_msg({"message_type": 2, "item_list": []}) is None, "bot 回显忽略")
81
77
 
82
78
 
@@ -84,10 +80,8 @@ def test_feishu():
84
80
  print("\n【4】飞书")
85
81
  from foxclaw.channels.feishu import FeishuAdapter
86
82
  fs = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="my_secret_key_123")
87
- # URL 验证
88
83
  r = fs.handle_event({"type": "url_verification", "challenge": "c1"})
89
84
  check(r.get("challenge") == "c1", "URL 验证 challenge")
90
- # 加密解密端到端
91
85
  def encrypt(plaintext, key_str):
92
86
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
93
87
  key = hashlib.sha256(key_str.encode()).digest()
@@ -100,10 +94,8 @@ def test_feishu():
100
94
  original = {"type": "event", "x": "测试"}
101
95
  dec = fs.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123"))
102
96
  check(dec == original, "AES 加密解密端到端")
103
- # 错误密钥明确报错
104
97
  fs2 = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="wrong")
105
98
  check(fs2.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123")) is None, "错误密钥解密失败")
106
- # 消息标准化
107
99
  event = {"header": {"event_type": "im.message.receive_v1"},
108
100
  "event": {"message": {"message_id": "om1", "chat_id": "oc1", "chat_type": "p2p",
109
101
  "content": json.dumps({"text": "查日程"}), "create_time": "1718900000000"},
@@ -141,7 +133,7 @@ def test_closed_loop():
141
133
 
142
134
  import tempfile, shutil
143
135
  data_dir = tempfile.mkdtemp(prefix="foxclaw_ch_test_")
144
- shutil.rmtree(data_dir, ignore_errors=True) # 确保干净起步(防残留幂等记录污染)
136
+ shutil.rmtree(data_dir, ignore_errors=True)
145
137
  os_ = Foxclaw(data_dir=data_dir, shadow_rounds=50)
146
138
  disp = ChannelDispatcher(os_.supervisor)
147
139
  mock = Mock(); disp.register_channel(mock)
@@ -151,12 +143,10 @@ def test_closed_loop():
151
143
  sender_id="u1", conversation_id="c1")
152
144
  r1 = await disp.handle(msg)
153
145
  check(r1["replied"], "闭环:收→主管→回")
154
- # 幂等:同消息重发只处理一次
155
146
  r2 = await disp.handle(msg)
156
147
  check(r2.get("idempotent_skip"), "重发消息被幂等跳过")
157
148
  check(len(mock.sent) == 1, "重发只回复一次")
158
149
  asyncio.run(go())
159
- # 清理
160
150
  import shutil
161
151
  shutil.rmtree(data_dir, ignore_errors=True)
162
152
 
@@ -9,7 +9,6 @@ def test_config_layering():
9
9
 
10
10
  from foxclaw.core.config import LayeredConfig, load_policy_layer
11
11
 
12
- # 1) LayeredConfig:标量高层覆盖;集合三层并集(只增不减);policy_locked
13
12
  cfg = LayeredConfig(policy={"model": "P", "deny": ["a"]},
14
13
  user={"model": "U", "deny": ["b"]},
15
14
  runtime={"model": "R", "deny": ["c"], "x": 1})
@@ -19,7 +18,6 @@ def test_config_layering():
19
18
  assert cfg.policy_locked("deny") == {"a"}, "policy 层锁定项"
20
19
  print("[1] LayeredConfig 标量覆盖 + 集合并集 + policy_locked OK")
21
20
 
22
- # 2) load_policy_layer:env + policy.json 合并;默认空
23
21
  d = tempfile.mkdtemp(prefix="e1_")
24
22
  try:
25
23
  os.environ["FOXCLAW_POLICY_DENY"] = "gmail_send, danger_tool"
@@ -28,7 +26,6 @@ def test_config_layering():
28
26
  assert set(pl["deny"]) == {"gmail_send", "danger_tool", "file_delete"}, pl
29
27
  print(f"[2] load_policy_layer(env + policy.json) = {pl['deny']} OK")
30
28
 
31
- # 3) SkillPolicy:policy 层 deny 生效 + undeny 撤不掉 + 从盘 user deny 生效(引用未断) + user 可撤
32
29
  (Path(d) / "skill_policy.json").write_text(
33
30
  json.dumps({"deny": ["user_denied"], "allow": []}), encoding="utf-8")
34
31
  from foxclaw.skills.policy import SkillPolicy
@@ -45,7 +42,6 @@ def test_config_layering():
45
42
  os.environ.pop("FOXCLAW_POLICY_DENY", None)
46
43
  shutil.rmtree(d, ignore_errors=True)
47
44
 
48
- # 4) behavior-preserving:无 policy 层(无 env、无 policy.json)→ SkillPolicy 行为 = 现状
49
45
  d2 = tempfile.mkdtemp(prefix="e1b_")
50
46
  try:
51
47
  from foxclaw.skills.policy import SkillPolicy