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,97 @@
1
+ """
2
+ 用 FastAPI TestClient 在同进程内验证 API 端点。
3
+ 绕过沙箱对后台进程/loopback 的限制,直接走 ASGI。
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+ sys.path.insert(0, str(Path(__file__).parent.parent))
8
+
9
+ import os
10
+ import tempfile
11
+ import json
12
+ from fastapi.testclient import TestClient
13
+
14
+
15
+ def show(title, data, limit=None):
16
+ print(f"\n【{title}】")
17
+ s = json.dumps(data, ensure_ascii=False, indent=2)
18
+ if limit:
19
+ s = "\n".join(s.splitlines()[:limit])
20
+ print(s)
21
+
22
+
23
+ def test_api():
24
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
25
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
26
+ from foxclaw.api import app
27
+
28
+ with TestClient(app) as c:
29
+ print("="*64)
30
+ print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
31
+ print("="*64)
32
+
33
+ r = c.get("/api/status")
34
+ print(f"\n[GET /api/status] HTTP {r.status_code}")
35
+ data = r.json()
36
+ for aid, s in data.items():
37
+ tc = s["thought_chain"]
38
+ print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
39
+
40
+ print("\n[POST /api/task] 连发 7 个财务任务触发校准")
41
+ for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
42
+ r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
43
+ d = r.json()
44
+ err = d.get("error", "ok")
45
+ print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
46
+
47
+ r = c.get("/api/harness/finance/status")
48
+ show("GET /api/harness/finance/status", r.json())
49
+
50
+ r = c.get("/api/thoughtchain/finance/chain")
51
+ nodes = r.json()["nodes"]
52
+ print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
53
+ for n in nodes[-5:]:
54
+ print(f" {n['type']:<16} {n['harness_status']:<9} "
55
+ f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
56
+
57
+ r = c.get("/api/thoughtchain/finance/trend?limit=10")
58
+ trend = r.json()["trend"]
59
+ print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
60
+ print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
61
+
62
+ r = c.get("/api/harness/finance/policy")
63
+ pol = r.json()
64
+ print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
65
+ f"{len(pol.get('rules',[]))} 规则")
66
+
67
+ r = c.get("/api/preferences")
68
+ assert r.status_code == 200 and "preferences" in r.json(), r.text
69
+ r = c.delete("/api/preferences/nonexistent_id")
70
+ assert r.status_code == 200 and r.json().get("ok") is False, r.text
71
+ print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
72
+
73
+ r = c.put("/api/profile", json={"nickname": " snow "})
74
+ assert r.status_code == 200 and r.json().get("nickname") == "snow", r.text
75
+ r = c.get("/api/profile")
76
+ assert r.json().get("nickname") == "snow", r.text
77
+ c.put("/api/profile", json={"nickname": "snow2"})
78
+ r = c.get("/api/preferences")
79
+ nicks = [p["content"] for p in r.json()["preferences"] if "称呼我为" in p["content"]]
80
+ assert nicks == ["用户偏好:称呼我为 snow2"], nicks
81
+ print("\n[GET/PUT /api/profile] 称呼写入/读回/替换 ok")
82
+
83
+ r = c.get("/api/system/info")
84
+ info = r.json()
85
+ assert r.status_code == 200 and "update" in info, r.text
86
+ up = info["update"]
87
+ assert {"enabled", "current", "latest", "update_available"} <= set(up), up
88
+ assert up["enabled"] is False and up["update_available"] is False, up
89
+ print("\n[GET /api/system/info] update 字段存在,默认关闭")
90
+
91
+ print("\n" + "="*64)
92
+ print("✓ API 端到端验证完成 · 所有端点返回真实数据")
93
+ print("="*64)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ test_api()
@@ -0,0 +1,79 @@
1
+ """
2
+ API 可选鉴权回归
3
+
4
+ 验证 FOXCLAW_API_KEY:
5
+ · 未设 → /api/* 开放(本地默认,既有行为不变);
6
+ · 设了 → /api/* 与 /ws/* 需带正确 X-API-Key,否则 401;前端与 /docs 始终开放。
7
+ 中间件每请求读环境变量,故可在同一 TestClient 内切换验证。
8
+ """
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
14
+
15
+ import tempfile
16
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
17
+ from fastapi.testclient import TestClient
18
+ from foxclaw.api import app
19
+
20
+
21
+ def check(cond, msg):
22
+ if not cond:
23
+ raise AssertionError(f"✗ {msg}")
24
+ print(f" ✓ {msg}")
25
+
26
+
27
+ def main():
28
+ print("=" * 56)
29
+ print("API 可选鉴权回归")
30
+ print("=" * 56)
31
+ os.environ.pop("FOXCLAW_API_KEY", None)
32
+ with TestClient(app) as c:
33
+ print("\n【1】未设 FOXCLAW_API_KEY:开放(本地默认)")
34
+ check(c.get("/api/status").status_code == 200, "/api/status 开放(200)")
35
+
36
+ print("\n【2】设了 FOXCLAW_API_KEY:/api/* 受保护")
37
+ os.environ["FOXCLAW_API_KEY"] = "secret-123"
38
+ check(c.get("/api/status").status_code == 401, "无 X-API-Key → 401")
39
+ check(c.get("/api/status", headers={"X-API-Key": "wrong"}).status_code == 401,
40
+ "错误 key → 401")
41
+ check(c.get("/api/status", headers={"X-API-Key": "secret-123"}).status_code == 200,
42
+ "正确 key → 200")
43
+
44
+ print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
45
+ rejected = False
46
+ try:
47
+ with c.websocket_connect("/ws/harness"):
48
+ pass
49
+ except Exception:
50
+ rejected = True
51
+ check(rejected, "WS 无 key → 握手被拒")
52
+ connected = False
53
+ try:
54
+ with c.websocket_connect("/ws/harness?key=secret-123"):
55
+ connected = True
56
+ except Exception:
57
+ connected = False
58
+ check(connected, "WS 带正确 ?key= → 连上")
59
+
60
+ print("\n【4】开鉴权时前端 / 文档仍开放")
61
+ check(c.get("/docs").status_code == 200, "/docs 无需 key(200)")
62
+ r_root = c.get("/")
63
+ check(r_root.status_code in (200, 404), f"/ 非 401(前端不被 key 挡, {r_root.status_code})")
64
+
65
+ print("\n【5】撤销 key → 恢复开放")
66
+ os.environ.pop("FOXCLAW_API_KEY", None)
67
+ check(c.get("/api/status").status_code == 200, "撤 key 后 /api/status 恢复开放")
68
+
69
+ print("\n" + "=" * 56)
70
+ print("API 鉴权回归通过")
71
+ print("=" * 56)
72
+
73
+
74
+ def test_api_auth():
75
+ main()
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
@@ -0,0 +1,69 @@
1
+ """API 层 Host 头校验(DNS rebinding 防护)。
2
+
3
+ 攻击模型:本机 `foxclaw serve`(回环监听、无 API key)时,用户被诱导打开 evil.com;
4
+ evil.com 解析到 127.0.0.1,页面内 fetch 打本机 /api/*,同源成立 → 可读写本地数据面。
5
+ 防线是校验 Host 头:浏览器发请求时 Host 必须与被访问域名一致,DNS 改解析也绕不过。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from types import SimpleNamespace
12
+
13
+ import pytest
14
+
15
+ from foxclaw.api.middleware import _api_key_guard, _host_header_allowed
16
+
17
+
18
+ @pytest.mark.parametrize("host", [
19
+ "127.0.0.1:8000", "localhost:8000", "localhost", "TESTCLIENT",
20
+ "[::1]:8000", "192.168.1.9:8000", "10.0.0.2", "mypc", "",
21
+ ])
22
+ def test_allows_local_and_lan_hosts(host: str) -> None:
23
+ assert _host_header_allowed(host) is True
24
+
25
+
26
+ @pytest.mark.parametrize("host", [
27
+ "evil.com", "attacker.example.org:8000", "sub.evil.com:80",
28
+ ])
29
+ def test_rejects_public_domain_hosts(host: str) -> None:
30
+ assert _host_header_allowed(host) is False
31
+
32
+
33
+ def test_allowlist_escape_hatch(monkeypatch) -> None:
34
+ monkeypatch.setenv("FOXCLAW_ALLOWED_HOSTS", "my.host.net, *.corp.example")
35
+ assert _host_header_allowed("my.host.net") is True
36
+ assert _host_header_allowed("a.corp.example") is True
37
+ assert _host_header_allowed("evil.com") is False
38
+
39
+
40
+ def _request(path: str, host: str, client: str = "127.0.0.1"):
41
+ return SimpleNamespace(
42
+ url=SimpleNamespace(path=path),
43
+ client=SimpleNamespace(host=client),
44
+ headers={"host": host, "X-API-Key": ""},
45
+ method="GET",
46
+ )
47
+
48
+
49
+ async def _ok(_request):
50
+ return SimpleNamespace(status_code=200)
51
+
52
+
53
+ @pytest.mark.parametrize("path", ["/api/task", "/knowledge/ingest", "/config/llm"])
54
+ def test_loopback_with_public_host_is_forbidden(path: str, monkeypatch) -> None:
55
+ monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
56
+ resp = asyncio.run(_api_key_guard(_request(path, "evil.com"), _ok))
57
+ assert resp.status_code == 403, "本机客户端 + 公网域名 Host 必须拒绝"
58
+
59
+
60
+ def test_loopback_with_local_host_passes(monkeypatch) -> None:
61
+ monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
62
+ resp = asyncio.run(_api_key_guard(_request("/api/task", "127.0.0.1:8000"), _ok))
63
+ assert resp.status_code == 200
64
+
65
+
66
+ def test_probe_paths_not_affected(monkeypatch) -> None:
67
+ monkeypatch.delenv("FOXCLAW_API_KEY", raising=False)
68
+ resp = asyncio.run(_api_key_guard(_request("/health", "evil.com"), _ok))
69
+ assert resp.status_code == 200, "探针不在数据面防护范围"
@@ -0,0 +1,151 @@
1
+ """聊天附件(多模态渲染·步骤7)回归测试:文件上传/下载、正文提取、护栏、LLM 上下文注入、会话落盘。
2
+
3
+ 全离线:锁 stub 模型 + stub 嵌入 + 临时数据目录,不碰真实 API / 不加载真实向量模型。
4
+ 与 test_multimodal.py(图片通道)互补:这里覆盖「文件」通道(图片走 images base64,不经过附件)。
5
+ """
6
+ import os, sys, tempfile, shutil
7
+ from pathlib import Path
8
+ sys.path.insert(0, str(Path(__file__).parent.parent))
9
+
10
+ for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
11
+ "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
12
+ os.environ.pop(_k, None)
13
+ os.environ["HKC_EMBEDDING"] = "stub"
14
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
15
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
16
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
17
+ os.environ["FOXCLAW_PROVIDER"] = ""
18
+ os.environ["FOXCLAW_MODEL"] = ""
19
+ os.environ["FOXCLAW_MCP_SERVER"] = "0"
20
+ os.environ["HF_HUB_OFFLINE"] = "1"
21
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
22
+
23
+ import pytest
24
+ from fastapi.testclient import TestClient
25
+
26
+ from foxclaw.api import app, _sanitize_attachments
27
+ from foxclaw.api.routes.task import _MAX_ATTACHMENTS, _ATTACH_TEXT_MAX, _ATTACH_TEXT_TOTAL
28
+ from foxclaw.agents.base import CognitiveAgent
29
+ from foxclaw.core.sessions import SessionStore
30
+
31
+
32
+ @pytest.fixture()
33
+ def store():
34
+ d = tempfile.mkdtemp(prefix="att_sess_")
35
+ try:
36
+ yield SessionStore(Path(d) / "sessions")
37
+ finally:
38
+ shutil.rmtree(d, ignore_errors=True)
39
+
40
+
41
+ def test_sanitize_attachments():
42
+ many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
43
+ assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
44
+
45
+ one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
46
+ assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
47
+
48
+ assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
49
+
50
+ big = "x" * (_ATTACH_TEXT_MAX + 1000)
51
+ four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
52
+ assert len(four) == 4
53
+ assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
54
+ assert four[3]["text"] == ""
55
+
56
+ assert _sanitize_attachments("notalist") == []
57
+ assert _sanitize_attachments([1, "x"]) == []
58
+ assert _sanitize_attachments([{"name": "", "text": ""}]) == []
59
+ assert _sanitize_attachments(None) == []
60
+
61
+ out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
62
+ assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
63
+ print("护栏(数量/单文件/累计/类型清洗/边界)OK")
64
+
65
+
66
+ def test_attachments_context():
67
+ atts = [
68
+ {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "", "text": "hello world"},
69
+ {"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "", "text": ""},
70
+ ]
71
+ ctx = CognitiveAgent._attachments_context(atts)
72
+ assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
73
+ assert "【附件「b.txt」" not in ctx
74
+ assert "b.pdf" in ctx and "无法提取" in ctx
75
+ assert CognitiveAgent._attachments_context([]) == ""
76
+ assert CognitiveAgent._attachments_context(None) == ""
77
+ assert CognitiveAgent._attachments_context([1, "x"]) == ""
78
+ print("LLM 上下文注入(正文/降级/空)OK")
79
+
80
+
81
+ def test_sessions_append_attachments(store):
82
+ s = store.create()
83
+ atts = [
84
+ {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc", "text": "正文不落盘"},
85
+ {"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "/api/attachments/def", "text": ""},
86
+ ]
87
+ assert store.append(s["id"], "user", "读一下这两个文件", attachments=atts)
88
+ d = store.get(s["id"])
89
+ m = d["messages"][0]
90
+ assert m["role"] == "user" and m["content"] == "读一下这两个文件"
91
+ assert len(m["attachments"]) == 2
92
+ assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
93
+ assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
94
+ assert store.append(s["id"], "assistant", "好")
95
+ assert "attachments" not in store.get(s["id"])["messages"][1]
96
+ print("会话落盘(轻字段/不存 text/向后兼容)OK")
97
+
98
+
99
+ def test_upload_and_download():
100
+ with TestClient(app) as c:
101
+ r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
102
+ assert r.status_code == 200, r.text
103
+ a = r.json()
104
+ assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
105
+ assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
106
+
107
+ r2 = c.get(a["url"])
108
+ assert r2.status_code == 200
109
+ assert r2.content.decode("utf-8") == "今天学了三件事"
110
+
111
+ r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
112
+ assert r3.status_code == 200 and r3.json()["text"] == ""
113
+
114
+ assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
115
+ big = b"x" * (21 * 1024 * 1024)
116
+ assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
117
+
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
121
+ print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
122
+
123
+
124
+ def test_chat_stream_with_attachments():
125
+ import foxclaw.api as _api
126
+ with TestClient(app) as c:
127
+ up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
128
+ att = up.json()
129
+ sid = _api.os_instance().sessions.create()["id"]
130
+ payload = {
131
+ "task": "帮我总结要点",
132
+ "agent_id": "supervisor",
133
+ "session_id": sid,
134
+ "attachments": [{"name": att["name"], "size": att["size"], "mime": att["mime"],
135
+ "url": att["url"], "text": att["text"]}],
136
+ }
137
+ r = c.post("/api/chat/stream", json=payload)
138
+ assert r.status_code == 200, r.text[:200]
139
+ assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
140
+
141
+ d = _api.os_instance().sessions.get(sid)
142
+ assert d is not None, "会话未落盘"
143
+ um = d["messages"][0]
144
+ assert um["role"] == "user" and um["content"] == "帮我总结要点"
145
+ assert um["attachments"][0]["name"] == "要点.txt"
146
+ assert "text" not in um["attachments"][0]
147
+ print("/chat/stream 带附件端到端(SSE + 会话落盘)OK")
148
+
149
+
150
+ if __name__ == "__main__":
151
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,116 @@
1
+ """审计落盘的哈希链 + recovery 日志脱敏的回归。
2
+
3
+ 审计文件是纯 append 明文,谁能写谁就能改历史;哈希链不改可见性,只让「改一笔 / 删中间
4
+ 一笔」在 verify 里暴露。recovery 日志则是被回放读回、长期留在磁盘上的工具调用记录,
5
+ 工具参数里带 token 的情况很常见。
6
+ """
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent.parent))
12
+
13
+
14
+ def _write_chain(path: Path, records) -> None:
15
+ from foxclaw.core import audit_chain
16
+
17
+ prev = audit_chain.GENESIS
18
+ lines = []
19
+ for r in records:
20
+ rec = audit_chain.link(r, prev)
21
+ prev = rec["hash"]
22
+ lines.append(json.dumps(rec, ensure_ascii=False))
23
+ path.parent.mkdir(parents=True, exist_ok=True)
24
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
25
+
26
+
27
+ def test_chain_passes(tmp_path):
28
+ from foxclaw.core import audit_chain
29
+
30
+ p = tmp_path / "a.jsonl"
31
+ _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}, {"verdict": "success"}])
32
+ ok, n, msg = audit_chain.verify(p)
33
+ assert ok and n == 3, msg
34
+
35
+
36
+ def test_tampered_line_detected(tmp_path):
37
+ from foxclaw.core import audit_chain
38
+
39
+ p = tmp_path / "a.jsonl"
40
+ _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
41
+ lines = p.read_text(encoding="utf-8").splitlines()
42
+ rec = json.loads(lines[0])
43
+ rec["verdict"] = "allow"
44
+ lines[0] = json.dumps(rec, ensure_ascii=False)
45
+ p.write_text("\n".join(lines) + "\n", encoding="utf-8")
46
+
47
+ ok, _, msg = audit_chain.verify(p)
48
+ assert not ok and "被改" in msg, msg
49
+
50
+
51
+ def test_deleted_middle_line_detected(tmp_path):
52
+ from foxclaw.core import audit_chain
53
+
54
+ p = tmp_path / "a.jsonl"
55
+ _write_chain(p, [{"i": 0}, {"i": 1}, {"i": 2}])
56
+ lines = p.read_text(encoding="utf-8").splitlines()
57
+ p.write_text("\n".join([lines[0], lines[2]]) + "\n", encoding="utf-8")
58
+
59
+ ok, _, msg = audit_chain.verify(p)
60
+ assert not ok and "链断" in msg, msg
61
+
62
+
63
+ def test_resume_after_restart(tmp_path):
64
+ """进程重启后内存链尾丢失,新 auditor 应从文件尾部续链,而不是重开 GENESIS。"""
65
+ from foxclaw.core.governance_audit import GovernanceAuditor
66
+
67
+ p = tmp_path / "governance.jsonl"
68
+ GovernanceAuditor(sink_path=p).record(
69
+ layer="skill", actor="office", action="write_note", verdict="success")
70
+ aud = GovernanceAuditor(sink_path=p)
71
+ aud.record(layer="skill", actor="office", action="write_note", verdict="success")
72
+
73
+ ok, n, msg = aud.verify()
74
+ assert ok and n == 2, msg
75
+ recs = aud.read()
76
+ assert recs[1]["hash"] != recs[0]["hash"]
77
+
78
+
79
+ def test_legacy_lines_skipped(tmp_path):
80
+ """老文件(无 hash)不能因为「没链」就报错;新记录从 GENESIS 起头。"""
81
+ from foxclaw.core import audit_chain
82
+
83
+ p = tmp_path / "a.jsonl"
84
+ p.write_text('{"legacy": true}\n', encoding="utf-8")
85
+ assert audit_chain.verify(p)[0] is True
86
+ assert audit_chain.tail_hash(p) == audit_chain.GENESIS
87
+
88
+
89
+ def test_tail_hash_ignores_torn_line(tmp_path):
90
+ from foxclaw.core import audit_chain
91
+
92
+ p = tmp_path / "a.jsonl"
93
+ p.write_text('{"a": 1, "hash": "h1"}\n{"half', encoding="utf-8")
94
+ assert audit_chain.tail_hash(p) == "h1"
95
+
96
+
97
+ def test_recovery_step_args_and_result_redacted():
98
+ from types import SimpleNamespace
99
+
100
+ from foxclaw.agents.mixins.tool_exec import _record_step, _step_key
101
+
102
+ key = "sk-" + "a" * 24
103
+ got = {}
104
+
105
+ class _Rec:
106
+ @staticmethod
107
+ def record_step(tid, rec):
108
+ got.update(rec)
109
+
110
+ agent = SimpleNamespace(_recovery=_Rec(), _session_id="s1")
111
+ _record_step(agent, {"name": "http", "args": {"api_key": key}},
112
+ f"ok token=sk-{'b' * 24}")
113
+
114
+ assert "sk-" not in json.dumps(got["args"]), got["args"]
115
+ assert "[已脱敏]" in got["result"], got["result"]
116
+ assert got["key"] == _step_key("http", {"api_key": key})
@@ -0,0 +1,130 @@
1
+ """审计落盘 sink(audit_sink)测试 —— P0-3 配套
2
+
3
+ 覆盖:
4
+ 1. 单元层:EventSpine + sink 直连 —— JSONL 格式、字段齐全、追加语义、开关、坏行容错
5
+ 2. 端到端层:真实 Foxclaw 装配 → 共享写入 → 文件里出现完整记录(接线正确性)
6
+ """
7
+ import os
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).parent.parent))
14
+
15
+ _FAILS = []
16
+ _N = 0
17
+
18
+
19
+ def check(cond, msg):
20
+ global _N
21
+ _N += 1
22
+ print(f" {'✓' if cond else '✗'} {msg}")
23
+ if not cond:
24
+ _FAILS.append(msg)
25
+
26
+
27
+ def test_unit():
28
+ print("\n【1】单元层:EventSpine + sink 直连")
29
+ from foxclaw.kernel.spine import EventSpine
30
+ from foxclaw.core.audit_sink import SharedWriteAuditSink, attach, ENV_SWITCH
31
+
32
+ tmp = tempfile.mkdtemp(prefix="foxclaw_asink_")
33
+ try:
34
+ path = Path(tmp) / "audit" / "shared_write.jsonl"
35
+ spine = EventSpine()
36
+ sink = SharedWriteAuditSink(path)
37
+ spine.subscribe("memory.shared.write", sink)
38
+
39
+ spine.publish("memory.shared.write", {
40
+ "agent_id": "office", "type": "decision",
41
+ "tags": ["finance.transfer"], "allowed": False,
42
+ "reason": "office 的职责域不含 ['type:decision','finance.transfer']",
43
+ "mode": "audit", "content_sha": "0123456789abcdef",
44
+ "content_preview": "财务应给办公转账1000元"}, source="office")
45
+ spine.publish("memory.updated", {"x": 1})
46
+
47
+ check(path.exists(), "审计文件已创建(含 audit/ 目录)")
48
+ recs = sink.read()
49
+ check(len(recs) == 1, f"只落了本主题 1 条(非本主题被过滤):实际 {len(recs)}")
50
+ r = recs[0]
51
+ check(r["topic"] == "memory.shared.write", "topic 正确")
52
+ check(r["payload"]["allowed"] is False, "payload 原样保留(allowed=False 可判读)")
53
+ check(len(r["payload"]["content_sha"]) == 16, "content_sha 为 16 位哈希")
54
+ check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
55
+ check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
56
+
57
+ spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
58
+ "tags": ["truth_pipeline"], "allowed": True,
59
+ "reason": "命中全局允许 facet", "mode": "audit",
60
+ "content_sha": "ffffffffffffffff",
61
+ "content_preview": "真值"})
62
+ recs = sink.read()
63
+ check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
64
+ "追加写不覆盖旧行,顺序保持")
65
+
66
+ with open(path, "a", encoding="utf-8") as f:
67
+ f.write("{这不是JSON}\n")
68
+ check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
69
+
70
+ old = os.environ.get(ENV_SWITCH)
71
+ os.environ[ENV_SWITCH] = "0"
72
+ try:
73
+ check(attach(EventSpine(), tmp) is None, "FOXCLAW_SHARED_WRITE_AUDIT=0 → 不挂")
74
+ os.environ[ENV_SWITCH] = "1"
75
+ s2 = attach(EventSpine(), tmp)
76
+ check(s2 is not None and s2.path == Path(tmp) / "audit" / "shared_write.jsonl",
77
+ "开启时挂载且路径跟随 data_dir")
78
+ finally:
79
+ if old is None:
80
+ os.environ.pop(ENV_SWITCH, None)
81
+ else:
82
+ os.environ[ENV_SWITCH] = old
83
+ finally:
84
+ shutil.rmtree(tmp, ignore_errors=True)
85
+
86
+
87
+ def test_end_to_end():
88
+ print("\n【2】端到端:真实 Foxclaw 装配 → 共享写入 → 落盘")
89
+ tmp = tempfile.mkdtemp(prefix="foxclaw_asink_e2e_")
90
+ try:
91
+ from foxclaw import Foxclaw
92
+ osi = Foxclaw(data_dir=tmp, shadow_rounds=5)
93
+
94
+ check(osi.shared_write_audit is not None, "sink 已随装配挂上(默认开启)")
95
+ audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
96
+ check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
97
+
98
+ osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
99
+ tags=["preference"], shared=True)
100
+ osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
101
+ tags=["finance.transfer"], shared=True)
102
+
103
+ recs = osi.shared_write_audit.read()
104
+ check(len(recs) == 2, f"两笔共享写入都落盘:实际 {len(recs)}")
105
+ by_allowed = {r["payload"]["allowed"] for r in recs}
106
+ check(by_allowed == {True, False}, "allowed 的 True/False 都被原样记录(判读依据完整)")
107
+ denied = [r for r in recs if not r["payload"]["allowed"]]
108
+ check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
109
+ "越界那条可从文件直接判读")
110
+ spine_n = len(osi.event_spine.history("memory.shared.write", 100))
111
+ check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
112
+ finally:
113
+ shutil.rmtree(tmp, ignore_errors=True)
114
+
115
+
116
+ def main():
117
+ print("=" * 60)
118
+ print("审计落盘 sink(P0-3 配套)")
119
+ print("=" * 60)
120
+ test_unit()
121
+ test_end_to_end()
122
+ print("\n" + "=" * 60)
123
+ if _FAILS:
124
+ print(f"✗ {len(_FAILS)}/{_N} 项失败"); return 1
125
+ print(f"✓ 全部通过({_N} 项)")
126
+ return 0
127
+
128
+
129
+ if __name__ == "__main__":
130
+ raise SystemExit(main())