foxclaw 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,149 @@
1
+ """
2
+ 浏览器子系统(HIR-Browser 融入)测试 —— 全离线,不启动真实 chromium。
3
+
4
+ 覆盖:
5
+ 1. 核心感知/校验逻辑(FakeDriver):reducer 编号稳定 + navigate 判生效 + noop-click 判失效
6
+ 2. 动作类型校验:非法 type 抛错、from_dict 透传
7
+ 3. BrowserService async 包装(注入假 session):perceive/act 经专用工作线程返回结构化结果
8
+ 4. 可选依赖降级:available()=False 时 perceive/act 返回 available=False 而非崩溃
9
+ 5. close() 幂等 + 关停后再调用被拒
10
+
11
+ 真实浏览器端到端(需 playwright install chromium)不在回归里跑,避免 CI 依赖内核二进制;
12
+ 本测试证明的是"融合层的线程/降级/契约"正确,浏览器操作正确性由 HIR 原生逻辑保证。
13
+ """
14
+ import sys, asyncio
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ sys.path.insert(0, str(Path(__file__).parent.parent))
20
+
21
+ from foxclaw.browser import BrowserService
22
+ from foxclaw.browser.perception.dom_extractor import DomExtractor
23
+ from foxclaw.browser.perception.element_indexer import ElementIndexer
24
+ from foxclaw.browser.perception.semantic_reducer import SemanticReducer
25
+ from foxclaw.browser.action.action_verifier import ActionVerifier
26
+ from foxclaw.browser.action.action_types import WebAction, SUPPORTED_ACTIONS
27
+
28
+
29
+ class FakeDriver:
30
+ """模拟一个只有一个按钮的页面,可切换 url/title 模拟导航。"""
31
+ def __init__(self):
32
+ self.url = "https://fake.test/p1"; self.title = "P1"; self._e = ["h1"]
33
+ def query_interactive_elements(self, roles): return self._e
34
+ def describe_element(self, h): return {"role": "button", "text": "提交", "attrs": {}}
35
+ def get_visible_text(self): return "fake body"
36
+ def current_url(self): return self.url
37
+ def current_title(self): return self.title
38
+
39
+
40
+ class FakeSnapshot:
41
+ def to_dict(self): return {"url": "https://fake.test/p1", "title": "P1", "elements": [], "text_summary": ""}
42
+
43
+
44
+ class FakeSession:
45
+ """假会话:不碰 playwright,验证 Service 的线程/契约。"""
46
+ def __init__(self): self.closed = False
47
+ def snapshot(self): return FakeSnapshot()
48
+ def execute(self, action: WebAction):
49
+ return {"raw": {"navigated_to": action.target}, "semantic_state": FakeSnapshot().to_dict(),
50
+ "verified": {"success": True, "url_changed": True, "diff": {}}}
51
+ def close(self): self.closed = True
52
+
53
+
54
+ def test_core_logic():
55
+ print("\n【1】核心感知/校验逻辑(FakeDriver)")
56
+ d = FakeDriver()
57
+ reducer = SemanticReducer(DomExtractor(d), ElementIndexer())
58
+ snap = reducer.reduce()
59
+ assert snap.elements[0]["id"] == "1" and snap.elements[0]["role"] == "button"
60
+ print(f" ✓ reducer 分配编号: id={snap.elements[0]['id']} role={snap.elements[0]['role']}")
61
+
62
+ before = reducer.reduce()
63
+ d.url = "https://fake.test/p2"; d.title = "P2"
64
+ after = reducer.reduce()
65
+ v = ActionVerifier()
66
+ nav = v.verify(before, after, WebAction(type="navigate", target="https://fake.test/p2"))
67
+ assert nav["success"] and nav["url_changed"]
68
+ print(" ✓ navigate 判定生效(url 变化)")
69
+
70
+ noop = v.verify(reducer.reduce(), reducer.reduce(), WebAction(type="click", target="1"))
71
+ assert noop["success"] is False
72
+ print(" ✓ 页面无变化的 click 判定为未生效(抓静默失败)")
73
+
74
+
75
+ def test_action_types():
76
+ print("\n【2】动作类型契约")
77
+ assert set(SUPPORTED_ACTIONS) == {"navigate", "click", "type", "scroll", "extract", "wait"}
78
+ raised = False
79
+ try:
80
+ WebAction(type="explode")
81
+ except ValueError:
82
+ raised = True
83
+ assert raised, "非法动作类型必须抛 ValueError"
84
+ a = WebAction.from_dict({"type": "type", "target": "3", "value": "hi", "meta": {"x": 1}})
85
+ assert a.type == "type" and a.target == "3" and a.value == "hi" and a.meta == {"x": 1}
86
+ print(" ✓ 非法 type 抛错 + from_dict 透传 target/value/meta")
87
+
88
+
89
+ @pytest.mark.anyio
90
+ async def test_service_wrapper():
91
+ print("\n【3】BrowserService async 包装(注入假 session,经专用工作线程)")
92
+ svc = BrowserService()
93
+ fake = FakeSession()
94
+ svc._session = fake
95
+ svc.available = lambda: True
96
+
97
+ p = await svc.perceive()
98
+ assert p["ok"] and p["semantic_state"]["title"] == "P1"
99
+ print(f" ✓ perceive() 经工作线程返回语义结构: title={p['semantic_state']['title']}")
100
+
101
+ r = await svc.act({"type": "navigate", "target": "https://fake.test/p2"})
102
+ assert r["ok"] and r["verified"]["success"]
103
+ print(" ✓ act(navigate) 返回校验后结果 verified.success=True")
104
+
105
+ bad = await svc.act({"target": "3"})
106
+ assert bad["ok"] is False and "非法动作" in bad["error"]
107
+ print(" ✓ 非法动作(缺 type)被结构化拒绝")
108
+
109
+ await svc.close()
110
+ assert fake.closed, "close() 必须在工作线程上释放会话"
111
+ print(" ✓ close() 释放了会话")
112
+ await svc.close()
113
+ after_close = await svc.perceive()
114
+ assert after_close["ok"] is False
115
+ print(" ✓ close() 幂等 + 关停后调用被安全拒绝")
116
+
117
+
118
+ @pytest.mark.anyio
119
+ async def test_degradation():
120
+ print("\n【4】可选依赖降级(available=False)")
121
+ svc = BrowserService()
122
+ svc.available = staticmethod(lambda: False)
123
+ p = await svc.perceive()
124
+ assert p["ok"] is False and p["available"] is False and "playwright" in p["error"]
125
+ a = await svc.act({"type": "navigate", "target": "x"})
126
+ assert a["ok"] is False and a["available"] is False
127
+ print(" ✓ playwright 缺失时 perceive/act 结构化降级,不崩溃")
128
+ st = svc.status()
129
+ assert set(["available", "session_active", "closed"]).issubset(st.keys())
130
+ print(f" ✓ status() 暴露能力: {st}")
131
+
132
+
133
+ def main():
134
+ print("=" * 60)
135
+ print("浏览器子系统 · HIR-Browser 融入(全离线)")
136
+ print("=" * 60)
137
+ test_core_logic()
138
+ test_action_types()
139
+ asyncio.run(test_service_wrapper())
140
+ asyncio.run(test_degradation())
141
+ print(f"\n (真实浏览器可用性: playwright {'已装' if BrowserService.available() else '未装'};"
142
+ f" 真实 e2e 需 playwright install chromium,不在回归内)")
143
+ print("\n" + "=" * 60)
144
+ print("✓ 浏览器子系统验证完成:核心逻辑 + async线程包装 + 降级 + 幂等close")
145
+ print("=" * 60)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -0,0 +1,180 @@
1
+ """浏览器请求级准入(跳转 / iframe / 子资源)与路由拦截。
2
+
3
+ 只测判定与回调,不起真浏览器:PlaywrightDriver 的模块顶层不 import playwright,
4
+ 用 `object.__new__` 绕过 __init__ 即可直接测 `_on_route`。
5
+ """
6
+
7
+ import pytest
8
+
9
+ from foxclaw.browser.action import url_policy
10
+ from foxclaw.browser.driver.playwright_driver import PlaywrightDriver
11
+
12
+
13
+ @pytest.mark.parametrize("url", [
14
+ "data:image/png;base64,iVBORw0KGgo=",
15
+ "blob:https://example.com/2f8c-4a",
16
+ "about:blank",
17
+ ])
18
+ def test_internal_schemes_are_allowed(monkeypatch, url):
19
+ """data:/blob:/about: 由页面自身产生、不过网络栈,拦了只会让内联图片裂掉。"""
20
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
21
+ assert url_policy.validate_request_url(url) == url
22
+
23
+
24
+ @pytest.mark.parametrize("url", [
25
+ "file:///C:/Windows/win.ini",
26
+ "javascript:alert(1)",
27
+ "ftp://example.com/x",
28
+ "chrome-extension://abcdef/page.html",
29
+ ])
30
+ def test_non_http_schemes_are_rejected(monkeypatch, url):
31
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
32
+ with pytest.raises(ValueError):
33
+ url_policy.validate_request_url(url)
34
+
35
+
36
+ @pytest.mark.parametrize("url", [
37
+ "http://169.254.169.254/latest/meta-data/",
38
+ "http://127.0.0.1:8080/admin",
39
+ "http://[::1]/",
40
+ "http://10.0.0.5/",
41
+ "http://192.168.1.1/",
42
+ ])
43
+ def test_private_addresses_are_rejected(monkeypatch, url):
44
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
45
+ with pytest.raises(ValueError):
46
+ url_policy.validate_request_url(url)
47
+
48
+
49
+ def test_allow_private_env_opens_the_gate(monkeypatch):
50
+ monkeypatch.setenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", "1")
51
+ assert url_policy.validate_request_url("http://127.0.0.1:8080/") == "http://127.0.0.1:8080/"
52
+
53
+
54
+ def test_public_ip_literal_passes_without_dns(monkeypatch):
55
+ """IP 字面量不该触发解析:断网环境下也得能判定。"""
56
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
57
+
58
+ def _boom(*_a, **_k):
59
+ raise AssertionError("IP 字面量不应该走 getaddrinfo")
60
+
61
+ monkeypatch.setattr(url_policy.socket, "getaddrinfo", _boom)
62
+ assert url_policy.validate_request_url("http://93.184.216.34/x") == "http://93.184.216.34/x"
63
+
64
+
65
+ def test_domain_resolving_to_internal_is_rejected(monkeypatch):
66
+ """域名指向内网必须拒。
67
+
68
+ 回归点:旧实现把 `raise ValueError` 写在自己那个 `try/except ValueError: continue`
69
+ 里,拒绝分支被同一个 except 吞掉 —— 「域名解析到内网」从来没生效过。
70
+ """
71
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
72
+ monkeypatch.setattr(url_policy, "_resolve_internal", lambda host: True)
73
+ with pytest.raises(ValueError) as ei:
74
+ url_policy.validate_request_url("http://intranet.example.com/x")
75
+ assert "内网" in str(ei.value)
76
+
77
+
78
+ def test_domain_resolving_to_public_passes(monkeypatch):
79
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
80
+ monkeypatch.setattr(url_policy, "_resolve_internal", lambda host: False)
81
+ assert url_policy.validate_request_url(
82
+ "https://example.com/a.png") == "https://example.com/a.png"
83
+
84
+
85
+ def test_resolution_is_cached_between_requests(monkeypatch):
86
+ """几十个子资源同域:只在首个请求上解析,后续走缓存。"""
87
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
88
+ url_policy._resolve_cache.clear()
89
+ calls = []
90
+
91
+ def _fake_getaddrinfo(host, *_a, **_k):
92
+ calls.append(host)
93
+ return [(2, 1, 6, "", ("93.184.216.34", 0))]
94
+
95
+ monkeypatch.setattr(url_policy.socket, "getaddrinfo", _fake_getaddrinfo)
96
+ for _ in range(5):
97
+ url_policy.validate_request_url("http://cdn.example.com/a.js")
98
+ assert calls == ["cdn.example.com"]
99
+ url_policy._resolve_cache.clear()
100
+
101
+
102
+ def test_short_ipv6_with_scope_is_not_mistaken(monkeypatch):
103
+ """解析结果可能带 scope id(fe80::1%eth0),不能因此算「非内网」。"""
104
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
105
+ url_policy._resolve_cache.clear()
106
+ monkeypatch.setattr(url_policy.socket, "getaddrinfo",
107
+ lambda host, *a, **k: [(10, 1, 6, "", ("fe80::1%eth0", 0, 0, 1))])
108
+ with pytest.raises(ValueError):
109
+ url_policy.validate_request_url("http://v6.example.com/")
110
+ url_policy._resolve_cache.clear()
111
+
112
+
113
+ class _FakeRoute:
114
+ def __init__(self, url):
115
+ self.request = type("Req", (), {"url": url})()
116
+ self.aborted = None
117
+ self.continued = False
118
+
119
+ def abort(self, reason=None):
120
+ self.aborted = reason or "abort"
121
+
122
+ def continue_(self):
123
+ self.continued = True
124
+
125
+
126
+ def _driver(monkeypatch):
127
+ monkeypatch.delenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", raising=False)
128
+ d = object.__new__(PlaywrightDriver)
129
+ d.blocked_requests = []
130
+ return d
131
+
132
+
133
+ def test_route_aborts_internal_subresource(monkeypatch):
134
+ """子资源打云元数据:这是「只校验 navigate 目标」漏掉的典型。"""
135
+ d = _driver(monkeypatch)
136
+ r = _FakeRoute("http://169.254.169.254/latest/meta-data/iam/")
137
+ d._on_route(r)
138
+ assert r.aborted == "blockedbyclient"
139
+ assert r.continued is False
140
+ assert d.blocked_requests and "169.254" in d.blocked_requests[0][0]
141
+
142
+
143
+ def test_route_continues_public_request(monkeypatch):
144
+ d = _driver(monkeypatch)
145
+ r = _FakeRoute("http://93.184.216.34/index.html")
146
+ d._on_route(r)
147
+ assert r.continued is True and r.aborted is None
148
+
149
+
150
+ def test_route_continues_data_uri(monkeypatch):
151
+ d = _driver(monkeypatch)
152
+ r = _FakeRoute("data:image/gif;base64,R0lGOD")
153
+ d._on_route(r)
154
+ assert r.continued is True and r.aborted is None
155
+
156
+
157
+ def test_route_aborts_redirect_target(monkeypatch):
158
+ """3xx 跳转的目标是独立请求,同样过这一层(否则公网页跳一次转就绕过了)。"""
159
+ d = _driver(monkeypatch)
160
+ r = _FakeRoute("http://127.0.0.1:9200/_cat/indices")
161
+ d._on_route(r)
162
+ assert r.aborted == "blockedbyclient"
163
+
164
+
165
+ def test_blocked_log_is_bounded(monkeypatch):
166
+ d = _driver(monkeypatch)
167
+ for i in range(70):
168
+ d._on_route(_FakeRoute(f"http://127.0.0.1/{i}"))
169
+ assert len(d.blocked_requests) <= 50
170
+
171
+
172
+ def test_route_survives_closed_session(monkeypatch):
173
+ """回调和 close 竞争时 continue_ 会抛,不能让它冒到 Playwright 的事件循环。"""
174
+ d = _driver(monkeypatch)
175
+
176
+ class _DeadRoute(_FakeRoute):
177
+ def continue_(self):
178
+ raise RuntimeError("Target closed")
179
+
180
+ d._on_route(_DeadRoute("http://93.184.216.34/x"))
@@ -0,0 +1,152 @@
1
+ """
2
+ 能力注册中枢(P1-3:God Object → Composition Root)测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. CapabilityRegistry:register / get / register_lazy(首次才构造且只构造一次) / has / names
6
+ 2. 打断两处直接环(结构):
7
+ · Agent 不再持有 _search_runtime,改持 _caps(经中枢解析 search_runtime)
8
+ · Orchestrator 不再把 agents 存为实例字段,改为经中枢解析的 property
9
+ 3. 行为保持:cross_request 经中枢解析 agents 仍工作;search_internal 经中枢解析 search_runtime
10
+ """
11
+ import os, sys, asyncio, tempfile, shutil
12
+ from pathlib import Path
13
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
14
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
15
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ from foxclaw.kernel import CapabilityRegistry
19
+
20
+
21
+ def test_registry_primitive():
22
+ print("\n【1】CapabilityRegistry:register / lazy / has / names")
23
+ reg = CapabilityRegistry()
24
+ reg.register("a", 123)
25
+ assert reg.get("a") == 123
26
+ assert reg.get("missing") is None and reg.get("missing", "d") == "d"
27
+
28
+ calls = {"n": 0}
29
+ def factory():
30
+ calls["n"] += 1
31
+ return {"built": True}
32
+ reg.register_lazy("b", factory)
33
+ assert reg.has("b") and "b" not in reg._caps, "惰性:注册后未构造"
34
+ v1 = reg.get("b"); v2 = reg.get("b")
35
+ assert v1 is v2 and calls["n"] == 1, "惰性只构造一次并缓存"
36
+ assert set(reg.names()) >= {"a", "b"}
37
+ print(f" ✓ register/get/lazy(构造 {calls['n']} 次)/has/names 均正确")
38
+
39
+
40
+ def test_cycles_broken_structural():
41
+ print("\n【2】结构:两处直接环被打断")
42
+ d = tempfile.mkdtemp(prefix="cap_os_")
43
+ try:
44
+ from foxclaw import Foxclaw
45
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
46
+ assert not hasattr(os_.finance, "_search_runtime"), "Agent 不应有 _search_runtime 直接引用"
47
+ assert os_.finance._caps is os_.capabilities, "Agent 改持能力中枢"
48
+ assert "agents" not in os_.orchestrator.__dict__, "orchestrator 不应把 agents 存为实例字段"
49
+ assert os_.orchestrator._caps is os_.capabilities
50
+ assert os_.orchestrator.agents is os_._agents, "orchestrator.agents 经中枢解析"
51
+ print(" ✓ Agent→search / Orchestrator→agents 的直接互持已改为中枢解析")
52
+ print(f" capabilities.names() = {os_.capabilities.names()}")
53
+ asyncio.run(os_.aclose())
54
+ finally:
55
+ shutil.rmtree(d, ignore_errors=True)
56
+
57
+
58
+ def test_behavior_preserved():
59
+ print("\n【3】行为保持:编排 + 双向搜索 经中枢解析仍工作")
60
+ d = tempfile.mkdtemp(prefix="cap_beh_")
61
+ try:
62
+ from foxclaw import Foxclaw
63
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
64
+ r = os_.supervisor.cross_request(
65
+ from_agent="admin", to_agent="finance", request_type="budget_request",
66
+ payload={"amount": 150, "category": "travel", "entity": "高铁", "account": "default"},
67
+ reason="出差")
68
+ assert r["status"] in ("approved", "confirm_required", "rejected"), f"裁决异常 {r}"
69
+ print(f" ✓ cross_request 经中枢解析 agents → 状态 {r['status']}")
70
+ res = os_.finance.search_internal("测试查询")
71
+ assert res is not None, "search_internal 应经中枢解析到 search_runtime"
72
+ assert os_.capabilities.get("search_runtime") is os_.search_runtime
73
+ print(" ✓ search_internal 经中枢解析 search_runtime(缓存一致)")
74
+ asyncio.run(os_.aclose())
75
+ finally:
76
+ shutil.rmtree(d, ignore_errors=True)
77
+
78
+
79
+ def test_lazy_build_once_concurrent():
80
+ print("\n【4】F1:并发下 per-name 惰性只构造一次(慢构造不持全局锁)")
81
+ import threading, time
82
+ reg = CapabilityRegistry()
83
+ calls = {"n": 0}
84
+ lock = threading.Lock()
85
+ def slow():
86
+ time.sleep(0.05)
87
+ with lock:
88
+ calls["n"] += 1
89
+ return object()
90
+ reg.register_lazy("x", slow)
91
+ results = []
92
+ def worker():
93
+ results.append(reg.get("x"))
94
+ ts = [threading.Thread(target=worker) for _ in range(12)]
95
+ for t in ts: t.start()
96
+ for t in ts: t.join()
97
+ assert calls["n"] == 1, f"并发下惰性应只构造 1 次,实得 {calls['n']}"
98
+ assert len({id(r) for r in results}) == 1, "12 线程应拿到同一实例"
99
+ reg.register_lazy("y", lambda: object())
100
+ assert reg.get("y") is reg.get("y")
101
+ print(" ✓ 12 线程并发 get 同一惰性 → 只构造 1 次、同一实例;不同能力独立")
102
+
103
+
104
+ def test_hkc_single_init_concurrent():
105
+ print("\n【5】F2:hkc 惰性属性并发首访只装配一次(防句柄泄漏)")
106
+ import threading
107
+ d = tempfile.mkdtemp(prefix="cap_hkc_")
108
+ try:
109
+ from foxclaw import Foxclaw
110
+ import foxclaw.knowledge as K
111
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
112
+ orig = K.init_hkc
113
+ cnt = {"n": 0}
114
+ clock = threading.Lock()
115
+ def counting(*a, **k):
116
+ with clock:
117
+ cnt["n"] += 1
118
+ return orig(*a, **k)
119
+ K.init_hkc = counting
120
+ try:
121
+ out = []
122
+ def w():
123
+ out.append(os_.hkc)
124
+ ts = [threading.Thread(target=w) for _ in range(10)]
125
+ for t in ts: t.start()
126
+ for t in ts: t.join()
127
+ assert cnt["n"] == 1, f"并发首访 hkc 应只装配 1 次,实得 {cnt['n']}"
128
+ assert len({id(o) for o in out}) == 1, "10 线程应拿到同一 HKCService"
129
+ print(" ✓ 10 线程并发首访 os_.hkc → init_hkc 只调 1 次")
130
+ finally:
131
+ K.init_hkc = orig
132
+ asyncio.run(os_.aclose())
133
+ finally:
134
+ shutil.rmtree(d, ignore_errors=True)
135
+
136
+
137
+ def main():
138
+ print("=" * 60)
139
+ print("能力注册中枢 · Composition Root(P1-3)")
140
+ print("=" * 60)
141
+ test_registry_primitive()
142
+ test_cycles_broken_structural()
143
+ test_behavior_preserved()
144
+ test_lazy_build_once_concurrent()
145
+ test_hkc_single_init_concurrent()
146
+ print("\n" + "=" * 60)
147
+ print("✓ P1-3 验证完成:能力中枢 + 两处直接环打断 + 行为保持")
148
+ print("=" * 60)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,185 @@
1
+ """渠道出站签名 / 入站验签。
2
+
3
+ 锁住:canonical 结构不被随手改、正文与时间戳都在签名覆盖内、重放被拒、header 名大小写
4
+ 无关、桥接层"签的就是发的"(content= 的字节)且签名失败 fail-closed 不发。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ import httpx
12
+ import pytest
13
+
14
+ from foxclaw.channels import OutboundMessage
15
+ from foxclaw.channels import signing as S
16
+ from foxclaw.channels.openclaw_bridge import OpenClawBridgeAdapter
17
+
18
+ SECRET = "test-shared-secret"
19
+
20
+
21
+ def _headers(secret: str = SECRET, body: bytes = b'{"a":1}', ts: float = 1_700_000_000):
22
+ signer = S.OutboundSigner(secret, key_id="k1")
23
+ return signer.headers(body, nonce="n0nce", now=ts)
24
+
25
+
26
+ def test_canonical_format_is_locked() -> None:
27
+ """canonical 是两端的协议:字段顺序/分隔符一改,对方全部验签失败。"""
28
+ line = S.canonical("1700000000", "abc", b"body").decode()
29
+ assert line == f"v1\n1700000000\nabc\n{S.body_digest(b'body')}"
30
+ assert len(line.split("\n")) == 4
31
+
32
+
33
+ def test_roundtrip_ok_and_body_tamper_detected() -> None:
34
+ body = b'{"text":"hi"}'
35
+ h = _headers(body=body)
36
+ ok, why = S.verify(h, body, SECRET, now=1_700_000_000)
37
+ assert (ok, why) == (True, "ok")
38
+ ok, why = S.verify(h, b'{"text":"transfer 5000"}', SECRET, now=1_700_000_000)
39
+ assert (ok, why) == (False, "bad_signature"), "正文被改必须失效"
40
+
41
+
42
+ def test_timestamp_skew_rejected() -> None:
43
+ body = b"{}"
44
+ h = _headers(body=body, ts=1_700_000_000)
45
+ ok, why = S.verify(h, body, SECRET, tolerance=300, now=1_700_000_000 + 301)
46
+ assert (ok, why) == (False, "expired")
47
+ ok, _ = S.verify(h, body, SECRET, tolerance=300, now=1_700_000_000 + 299)
48
+ assert ok is True
49
+
50
+
51
+ def test_signature_cannot_be_forged_with_wrong_secret() -> None:
52
+ body = b"{}"
53
+ h = _headers(body=body)
54
+ ok, why = S.verify(h, body, "another-secret", now=1_700_000_000)
55
+ assert (ok, why) == (False, "bad_signature")
56
+
57
+
58
+ def test_replay_guard_rejects_second_use() -> None:
59
+ body = b"{}"
60
+ h = _headers(body=body)
61
+ guard = S.ReplayGuard(ttl=300)
62
+ assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
63
+ assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (False, "replay")
64
+ h2 = S.OutboundSigner(SECRET).headers(body, nonce="other", now=1_700_000_000)
65
+ assert S.verify(h2, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
66
+
67
+
68
+ def test_verify_reason_codes() -> None:
69
+ body = b"{}"
70
+ assert S.verify({}, body, SECRET)[1] == "missing_header"
71
+ assert S.verify(_headers(body=body), body, "")[1] == "missing_secret"
72
+ bad = dict(_headers(body=body))
73
+ bad[S.SIGNATURE_HEADER] = "v2=deadbeef"
74
+ assert S.verify(bad, body, SECRET, now=1_700_000_000)[1] == "bad_version"
75
+ no_ts = dict(_headers(body=body))
76
+ no_ts[S.TIMESTAMP_HEADER] = "not-a-number"
77
+ assert S.verify(no_ts, body, SECRET)[1] == "bad_version"
78
+
79
+
80
+ def test_header_lookup_is_case_insensitive() -> None:
81
+ body = b"{}"
82
+ h = {k.lower(): v for k, v in _headers(body=body).items()}
83
+ assert S.verify(h, body, SECRET, now=1_700_000_000) == (True, "ok")
84
+
85
+
86
+ def test_signer_optional_key_id_and_disabled_state() -> None:
87
+ h = S.OutboundSigner(SECRET, key_id="k9").headers(b"{}")
88
+ assert h[S.KEY_ID_HEADER] == "k9"
89
+ assert S.SIGNATURE_HEADER in h and h[S.TIMESTAMP_HEADER].isdigit()
90
+ off = S.OutboundSigner("")
91
+ assert off.enabled is False
92
+ with pytest.raises(ValueError):
93
+ off.headers(b"{}")
94
+
95
+
96
+ def test_from_env_opt_in() -> None:
97
+ assert S.from_env({}) is None
98
+ s = S.from_env({S.ENV_SECRET: "abc", S.ENV_KEY_ID: "k2"})
99
+ assert s is not None and s.key_id == "k2" and s.enabled
100
+ assert S.from_env({S.ENV_SECRET: " "}) is None
101
+
102
+
103
+ class _Resp:
104
+ status_code = 200
105
+ text = ""
106
+
107
+ def json(self):
108
+ return {"message_id": "m1"}
109
+
110
+
111
+ class _Client:
112
+ """捕获真实发出的 (url, content, headers) —— 签名必须覆盖这些字节。"""
113
+
114
+ captured: dict = {}
115
+
116
+ def __init__(self, *a, **k):
117
+ pass
118
+
119
+ def __enter__(self):
120
+ return self
121
+
122
+ def __exit__(self, *a):
123
+ return False
124
+
125
+ def post(self, url, content=None, headers=None, timeout=None):
126
+ _Client.captured = {"url": url, "content": content, "headers": dict(headers or {})}
127
+ return _Resp()
128
+
129
+
130
+ @pytest.fixture
131
+ def fake_http(monkeypatch):
132
+ _Client.captured = {}
133
+ monkeypatch.setattr(httpx, "Client", _Client)
134
+ return _Client
135
+
136
+
137
+ def _msg() -> OutboundMessage:
138
+ return OutboundMessage(conversation_id="c1", text="hi", extra={"upstream_channel": "wechat"})
139
+
140
+
141
+ def test_bridge_signs_the_bytes_it_sends(fake_http) -> None:
142
+ bridge = OpenClawBridgeAdapter("http://gw.local", api_token="t",
143
+ signer=S.OutboundSigner(SECRET))
144
+ res = bridge.send(_msg())
145
+ assert res.ok and res.message_id == "m1"
146
+ cap = fake_http.captured
147
+ body = cap["content"]
148
+ assert isinstance(body, bytes) and json.loads(body)["text"] == "hi"
149
+ assert cap["headers"]["Content-Type"] == "application/json"
150
+ assert cap["headers"]["Authorization"] == "Bearer t"
151
+ ok, why = S.verify(cap["headers"], body, SECRET)
152
+ assert (ok, why) == (True, "ok")
153
+
154
+
155
+ def test_bridge_unsigned_without_secret(fake_http, monkeypatch) -> None:
156
+ monkeypatch.delenv(S.ENV_SECRET, raising=False)
157
+ bridge = OpenClawBridgeAdapter("http://gw.local")
158
+ assert bridge.signer is None
159
+ assert bridge.send(_msg()).ok
160
+ assert S.SIGNATURE_HEADER not in fake_http.captured["headers"]
161
+ assert fake_http.captured["headers"]["Content-Type"] == "application/json"
162
+
163
+
164
+ def test_bridge_fail_closed_when_signing_fails(fake_http) -> None:
165
+ class _Broken:
166
+ enabled = True
167
+
168
+ def headers(self, body, nonce="", now=None):
169
+ raise RuntimeError("kms down")
170
+
171
+ bridge = OpenClawBridgeAdapter("http://gw.local", signer=_Broken())
172
+ res = bridge.send(_msg())
173
+ assert res.ok is False and "签名失败" in res.error
174
+ assert fake_http.captured == {}, "签名失败必须不发(fail-closed)"
175
+
176
+
177
+ def test_bridge_verify_webhook(fake_http) -> None:
178
+ body = b'{"message":{"text":"hi"}}'
179
+ bridge = OpenClawBridgeAdapter("http://gw.local", signer=S.OutboundSigner(SECRET))
180
+ h = S.OutboundSigner(SECRET).headers(body)
181
+ assert bridge.verify_webhook(h, body) == (True, "ok")
182
+ assert bridge.verify_webhook(h, body + b" ") == (False, "bad_signature")
183
+
184
+ unsigned = OpenClawBridgeAdapter("http://gw.local", signer=S.OutboundSigner(""))
185
+ assert unsigned.verify_webhook(h, body) == (False, "missing_secret"), "没配密钥要拒收"