foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,87 @@
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
+ # 环境变量须在 import foxclaw.api 前设好(模块加载时按环境变量建网关/agent)
25
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
26
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
27
+ from foxclaw.api import app
28
+
29
+ with TestClient(app) as c:
30
+ print("="*64)
31
+ print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
32
+ print("="*64)
33
+
34
+ # 1. 系统状态
35
+ r = c.get("/api/status")
36
+ print(f"\n[GET /api/status] HTTP {r.status_code}")
37
+ data = r.json()
38
+ for aid, s in data.items():
39
+ tc = s["thought_chain"]
40
+ print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
41
+
42
+ # 2. 发任务(真实经过 ThoughtChain + Harness)
43
+ print("\n[POST /api/task] 连发 7 个财务任务触发校准")
44
+ for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
45
+ r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
46
+ d = r.json()
47
+ err = d.get("error", "ok")
48
+ print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
49
+
50
+ # 3. Harness 状态(校准后)
51
+ r = c.get("/api/harness/finance/status")
52
+ show("GET /api/harness/finance/status", r.json())
53
+
54
+ # 4. 推理链
55
+ r = c.get("/api/thoughtchain/finance/chain")
56
+ nodes = r.json()["nodes"]
57
+ print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
58
+ for n in nodes[-5:]:
59
+ print(f" {n['type']:<16} {n['harness_status']:<9} "
60
+ f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
61
+
62
+ # 5. 漂移趋势
63
+ r = c.get("/api/thoughtchain/finance/trend?limit=10")
64
+ trend = r.json()["trend"]
65
+ print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
66
+ print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
67
+
68
+ # 6. Policy
69
+ r = c.get("/api/harness/finance/policy")
70
+ pol = r.json()
71
+ print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
72
+ f"{len(pol.get('rules',[]))} 规则")
73
+
74
+ # 7. 偏好管理端点(F3):GET 列出结构;DELETE 不存在 id 优雅返回 ok=False(不崩)
75
+ r = c.get("/api/preferences")
76
+ assert r.status_code == 200 and "preferences" in r.json(), r.text
77
+ r = c.delete("/api/preferences/nonexistent_id")
78
+ assert r.status_code == 200 and r.json().get("ok") is False, r.text
79
+ print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
80
+
81
+ print("\n" + "="*64)
82
+ print("✓ API 端到端验证完成 · 所有端点返回真实数据")
83
+ print("="*64)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ test_api()
@@ -0,0 +1,83 @@
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
+ # 本测试要精确控制 FOXCLAW_API_KEY 的有/无;若 .env 带真实 key 会污染判定
14
+ # (见 foxclaw/api.py 中 FOXCLAW_SKIP_DOTENV 的说明),故关掉 .env。
15
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
16
+
17
+ import tempfile
18
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
19
+ from fastapi.testclient import TestClient
20
+ from foxclaw.api import app
21
+
22
+
23
+ def check(cond, msg):
24
+ if not cond:
25
+ raise AssertionError(f"✗ {msg}")
26
+ print(f" ✓ {msg}")
27
+
28
+
29
+ def main():
30
+ print("=" * 56)
31
+ print("API 可选鉴权回归")
32
+ print("=" * 56)
33
+ os.environ.pop("FOXCLAW_API_KEY", None)
34
+ with TestClient(app) as c:
35
+ print("\n【1】未设 FOXCLAW_API_KEY:开放(本地默认)")
36
+ check(c.get("/api/status").status_code == 200, "/api/status 开放(200)")
37
+
38
+ print("\n【2】设了 FOXCLAW_API_KEY:/api/* 受保护")
39
+ os.environ["FOXCLAW_API_KEY"] = "secret-123"
40
+ check(c.get("/api/status").status_code == 401, "无 X-API-Key → 401")
41
+ check(c.get("/api/status", headers={"X-API-Key": "wrong"}).status_code == 401,
42
+ "错误 key → 401")
43
+ check(c.get("/api/status", headers={"X-API-Key": "secret-123"}).status_code == 200,
44
+ "正确 key → 200")
45
+
46
+ print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
47
+ # 无 key → 握手被拒(端点 close 1008)
48
+ rejected = False
49
+ try:
50
+ with c.websocket_connect("/ws/harness"):
51
+ pass
52
+ except Exception:
53
+ rejected = True
54
+ check(rejected, "WS 无 key → 握手被拒")
55
+ # 带 ?key= → 连上
56
+ connected = False
57
+ try:
58
+ with c.websocket_connect("/ws/harness?key=secret-123"):
59
+ connected = True
60
+ except Exception:
61
+ connected = False
62
+ check(connected, "WS 带正确 ?key= → 连上")
63
+
64
+ print("\n【4】开鉴权时前端 / 文档仍开放")
65
+ check(c.get("/docs").status_code == 200, "/docs 无需 key(200)")
66
+ r_root = c.get("/")
67
+ check(r_root.status_code in (200, 404), f"/ 非 401(前端不被 key 挡, {r_root.status_code})")
68
+
69
+ print("\n【5】撤销 key → 恢复开放")
70
+ os.environ.pop("FOXCLAW_API_KEY", None)
71
+ check(c.get("/api/status").status_code == 200, "撤 key 后 /api/status 恢复开放")
72
+
73
+ print("\n" + "=" * 56)
74
+ print("API 鉴权回归通过")
75
+ print("=" * 56)
76
+
77
+
78
+ def test_api_auth():
79
+ main()
80
+
81
+
82
+ if __name__ == "__main__":
83
+ 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,167 @@
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
+ # ── 环境隔离:必须在 import foxclaw 之前(import 会读这些键初始化) ─────────────
11
+ for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
12
+ "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
13
+ os.environ.pop(_k, None)
14
+ os.environ["HKC_EMBEDDING"] = "stub"
15
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
16
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
17
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
18
+ os.environ["FOXCLAW_PROVIDER"] = "" # 不指定 provider → 无 key 时降级 stub(确定性)
19
+ os.environ["FOXCLAW_MODEL"] = ""
20
+ os.environ["FOXCLAW_MCP_SERVER"] = "0"
21
+ os.environ["HF_HUB_OFFLINE"] = "1" # 离线:HMR embedding 读本地缓存,不联网
22
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
23
+
24
+ import pytest
25
+ from fastapi.testclient import TestClient
26
+
27
+ from foxclaw.api import app, _sanitize_attachments
28
+ from foxclaw.api.routes.task import _MAX_ATTACHMENTS, _ATTACH_TEXT_MAX, _ATTACH_TEXT_TOTAL
29
+ from foxclaw.agents.base import CognitiveAgent
30
+ from foxclaw.core.sessions import SessionStore
31
+
32
+
33
+ @pytest.fixture()
34
+ def store():
35
+ d = tempfile.mkdtemp(prefix="att_sess_")
36
+ try:
37
+ yield SessionStore(Path(d) / "sessions")
38
+ finally:
39
+ shutil.rmtree(d, ignore_errors=True)
40
+
41
+
42
+ def test_sanitize_attachments():
43
+ # 数量上限
44
+ many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
45
+ assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
46
+
47
+ # 单文件正文截断到上限
48
+ one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
49
+ assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
50
+
51
+ # name 截断到 200
52
+ assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
53
+
54
+ # 累计正文封顶:单文件先截到 20000,4×20000 累计 60000 封顶 → 第 4 个正文被清空(名字仍在)
55
+ big = "x" * (_ATTACH_TEXT_MAX + 1000)
56
+ four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
57
+ assert len(four) == 4
58
+ assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
59
+ assert four[3]["text"] == ""
60
+
61
+ # 非列表 / 非 dict / 空 name+空 text → 全部剔除
62
+ assert _sanitize_attachments("notalist") == []
63
+ assert _sanitize_attachments([1, "x"]) == []
64
+ assert _sanitize_attachments([{"name": "", "text": ""}]) == []
65
+ assert _sanitize_attachments(None) == []
66
+
67
+ # 字段类型清洗:size 非数字 → 0;mime/url 转 str
68
+ out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
69
+ assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
70
+ print("护栏(数量/单文件/累计/类型清洗/边界)OK")
71
+
72
+
73
+ def test_attachments_context():
74
+ atts = [
75
+ {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "", "text": "hello world"},
76
+ {"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "", "text": ""},
77
+ ]
78
+ ctx = CognitiveAgent._attachments_context(atts)
79
+ assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
80
+ assert "【附件「b.txt」" not in ctx
81
+ assert "b.pdf" in ctx and "无法提取" in ctx # 非文本降级为「文件名 + 元数据」提示
82
+ assert CognitiveAgent._attachments_context([]) == ""
83
+ assert CognitiveAgent._attachments_context(None) == ""
84
+ assert CognitiveAgent._attachments_context([1, "x"]) == "" # 非 dict 全跳过
85
+ print("LLM 上下文注入(正文/降级/空)OK")
86
+
87
+
88
+ def test_sessions_append_attachments(store):
89
+ s = store.create()
90
+ atts = [
91
+ {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc", "text": "正文不落盘"},
92
+ {"name": "b.pdf", "size": 999, "mime": "application/pdf", "url": "/api/attachments/def", "text": ""},
93
+ ]
94
+ assert store.append(s["id"], "user", "读一下这两个文件", attachments=atts)
95
+ d = store.get(s["id"])
96
+ m = d["messages"][0]
97
+ assert m["role"] == "user" and m["content"] == "读一下这两个文件"
98
+ assert len(m["attachments"]) == 2
99
+ assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
100
+ assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
101
+ # 不带附件 → 无 attachments 字段(向后兼容)
102
+ assert store.append(s["id"], "assistant", "好")
103
+ assert "attachments" not in store.get(s["id"])["messages"][1]
104
+ print("会话落盘(轻字段/不存 text/向后兼容)OK")
105
+
106
+
107
+ def test_upload_and_download():
108
+ with TestClient(app) as c:
109
+ # 上传文本文件 → 提取正文
110
+ r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
111
+ assert r.status_code == 200, r.text
112
+ a = r.json()
113
+ assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
114
+ assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
115
+
116
+ # 下载 → 同一内容
117
+ r2 = c.get(a["url"])
118
+ assert r2.status_code == 200
119
+ assert r2.content.decode("utf-8") == "今天学了三件事"
120
+
121
+ # 二进制(不可提取)→ text 为空,降级
122
+ r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
123
+ assert r3.status_code == 200 and r3.json()["text"] == ""
124
+
125
+ # 空文件 → 400;超大 → 413
126
+ assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
127
+ big = b"x" * (21 * 1024 * 1024)
128
+ assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
129
+
130
+ # 路径穿越:HTTP 层规范化后到不了附件路由(404);非法 id 字符 → 400;合法但不存在 → 404
131
+ assert c.get("/api/attachments/../../etc/passwd").status_code == 404 # .. 被 URL 规范化,不匹配路由
132
+ assert c.get("/api/attachments/a.b").status_code == 400 # . 不在 id 白名单 [_A-Za-z0-9_-]
133
+ assert c.get("/api/attachments/deadbeef0000").status_code == 404 # 合法 id 但文件不存在
134
+ print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
135
+
136
+
137
+ def test_chat_stream_with_attachments():
138
+ import foxclaw.api as _api
139
+ with TestClient(app) as c:
140
+ # 先上传一个带正文的附件,拿到引用
141
+ up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
142
+ att = up.json()
143
+ # 会话须先存在,append 才会落盘(前端 ensureSession 同款:POST /api/sessions → 拿 id)
144
+ sid = _api._os_instance.sessions.create()["id"]
145
+ payload = {
146
+ "task": "帮我总结要点",
147
+ "agent_id": "supervisor",
148
+ "session_id": sid,
149
+ "attachments": [{"name": att["name"], "size": att["size"], "mime": att["mime"],
150
+ "url": att["url"], "text": att["text"]}],
151
+ }
152
+ r = c.post("/api/chat/stream", json=payload)
153
+ assert r.status_code == 200, r.text[:200]
154
+ assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
155
+
156
+ # 会话已落盘用户消息 + 附件元数据(不含 text)
157
+ d = _api._os_instance.sessions.get(sid)
158
+ assert d is not None, "会话未落盘"
159
+ um = d["messages"][0]
160
+ assert um["role"] == "user" and um["content"] == "帮我总结要点"
161
+ assert um["attachments"][0]["name"] == "要点.txt"
162
+ assert "text" not in um["attachments"][0]
163
+ print("/chat/stream 带附件端到端(SSE + 会话落盘)OK")
164
+
165
+
166
+ if __name__ == "__main__":
167
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,121 @@
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
+ # ── 链 ─────────────────────────────────────────────────────────────
28
+
29
+ def test_chain_passes(tmp_path):
30
+ from foxclaw.core import audit_chain
31
+
32
+ p = tmp_path / "a.jsonl"
33
+ _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}, {"verdict": "success"}])
34
+ ok, n, msg = audit_chain.verify(p)
35
+ assert ok and n == 3, msg
36
+
37
+
38
+ def test_tampered_line_detected(tmp_path):
39
+ from foxclaw.core import audit_chain
40
+
41
+ p = tmp_path / "a.jsonl"
42
+ _write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
43
+ lines = p.read_text(encoding="utf-8").splitlines()
44
+ rec = json.loads(lines[0])
45
+ rec["verdict"] = "allow" # 把一次拒绝抹成放行
46
+ lines[0] = json.dumps(rec, ensure_ascii=False)
47
+ p.write_text("\n".join(lines) + "\n", encoding="utf-8")
48
+
49
+ ok, _, msg = audit_chain.verify(p)
50
+ assert not ok and "被改" in msg, msg
51
+
52
+
53
+ def test_deleted_middle_line_detected(tmp_path):
54
+ from foxclaw.core import audit_chain
55
+
56
+ p = tmp_path / "a.jsonl"
57
+ _write_chain(p, [{"i": 0}, {"i": 1}, {"i": 2}])
58
+ lines = p.read_text(encoding="utf-8").splitlines()
59
+ p.write_text("\n".join([lines[0], lines[2]]) + "\n", encoding="utf-8")
60
+
61
+ ok, _, msg = audit_chain.verify(p)
62
+ assert not ok and "链断" in msg, msg
63
+
64
+
65
+ def test_resume_after_restart(tmp_path):
66
+ """进程重启后内存链尾丢失,新 auditor 应从文件尾部续链,而不是重开 GENESIS。"""
67
+ from foxclaw.core.governance_audit import GovernanceAuditor
68
+
69
+ p = tmp_path / "governance.jsonl"
70
+ GovernanceAuditor(sink_path=p).record(
71
+ layer="skill", actor="office", action="write_note", verdict="success")
72
+ aud = GovernanceAuditor(sink_path=p) # 模拟重启
73
+ aud.record(layer="skill", actor="office", action="write_note", verdict="success")
74
+
75
+ ok, n, msg = aud.verify()
76
+ assert ok and n == 2, msg
77
+ recs = aud.read()
78
+ assert recs[1]["hash"] != recs[0]["hash"]
79
+
80
+
81
+ def test_legacy_lines_skipped(tmp_path):
82
+ """老文件(无 hash)不能因为「没链」就报错;新记录从 GENESIS 起头。"""
83
+ from foxclaw.core import audit_chain
84
+
85
+ p = tmp_path / "a.jsonl"
86
+ p.write_text('{"legacy": true}\n', encoding="utf-8")
87
+ assert audit_chain.verify(p)[0] is True
88
+ assert audit_chain.tail_hash(p) == audit_chain.GENESIS
89
+
90
+
91
+ def test_tail_hash_ignores_torn_line(tmp_path):
92
+ from foxclaw.core import audit_chain
93
+
94
+ p = tmp_path / "a.jsonl"
95
+ p.write_text('{"a": 1, "hash": "h1"}\n{"half', encoding="utf-8")
96
+ assert audit_chain.tail_hash(p) == "h1"
97
+
98
+
99
+ # ── recovery 脱敏 ──────────────────────────────────────────────────
100
+
101
+ def test_recovery_step_args_and_result_redacted():
102
+ from types import SimpleNamespace
103
+
104
+ from foxclaw.agents.mixins.tool_exec import _record_step, _step_key
105
+
106
+ key = "sk-" + "a" * 24
107
+ got = {}
108
+
109
+ class _Rec:
110
+ @staticmethod
111
+ def record_step(tid, rec):
112
+ got.update(rec)
113
+
114
+ agent = SimpleNamespace(_recovery=_Rec(), _session_id="s1")
115
+ _record_step(agent, {"name": "http", "args": {"api_key": key}},
116
+ f"ok token=sk-{'b' * 24}")
117
+
118
+ assert "sk-" not in json.dumps(got["args"]), got["args"]
119
+ assert "[已脱敏]" in got["result"], got["result"]
120
+ # key 必须用未脱敏的 args 算,否则同一步骤前后 key 不一致(缓存/回放对不上)
121
+ assert got["key"] == _step_key("http", {"api_key": key})
@@ -0,0 +1,136 @@
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
+ # 非本主题的事件不应落盘(subscribe 前缀精确命中)
46
+ spine.publish("memory.updated", {"x": 1})
47
+
48
+ check(path.exists(), "审计文件已创建(含 audit/ 目录)")
49
+ recs = sink.read()
50
+ check(len(recs) == 1, f"只落了本主题 1 条(非本主题被过滤):实际 {len(recs)}")
51
+ r = recs[0]
52
+ check(r["topic"] == "memory.shared.write", "topic 正确")
53
+ check(r["payload"]["allowed"] is False, "payload 原样保留(allowed=False 可判读)")
54
+ check(len(r["payload"]["content_sha"]) == 16, "content_sha 为 16 位哈希")
55
+ check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
56
+ check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
57
+
58
+ # 追加语义:第二条接在后面,顺序保持
59
+ spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
60
+ "tags": ["truth_pipeline"], "allowed": True,
61
+ "reason": "命中全局允许 facet", "mode": "audit",
62
+ "content_sha": "ffffffffffffffff",
63
+ "content_preview": "真值"})
64
+ recs = sink.read()
65
+ check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
66
+ "追加写不覆盖旧行,顺序保持")
67
+
68
+ # 坏行容错:夹一行垃圾,read() 跳过而不是崩
69
+ with open(path, "a", encoding="utf-8") as f:
70
+ f.write("{这不是JSON}\n")
71
+ check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
72
+
73
+ # 开关:环境变量置 0 时 attach 不挂
74
+ old = os.environ.get(ENV_SWITCH)
75
+ os.environ[ENV_SWITCH] = "0"
76
+ try:
77
+ check(attach(EventSpine(), tmp) is None, "FOXCLAW_SHARED_WRITE_AUDIT=0 → 不挂")
78
+ os.environ[ENV_SWITCH] = "1"
79
+ s2 = attach(EventSpine(), tmp)
80
+ check(s2 is not None and s2.path == Path(tmp) / "audit" / "shared_write.jsonl",
81
+ "开启时挂载且路径跟随 data_dir")
82
+ finally:
83
+ if old is None:
84
+ os.environ.pop(ENV_SWITCH, None)
85
+ else:
86
+ os.environ[ENV_SWITCH] = old
87
+ finally:
88
+ shutil.rmtree(tmp, ignore_errors=True)
89
+
90
+
91
+ def test_end_to_end():
92
+ print("\n【2】端到端:真实 Foxclaw 装配 → 共享写入 → 落盘")
93
+ tmp = tempfile.mkdtemp(prefix="foxclaw_asink_e2e_")
94
+ try:
95
+ from foxclaw import Foxclaw
96
+ osi = Foxclaw(data_dir=tmp, shadow_rounds=5)
97
+
98
+ check(osi.shared_write_audit is not None, "sink 已随装配挂上(默认开启)")
99
+ audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
100
+ check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
101
+
102
+ # 一条合法 + 一条越界(audit 模式不拦截,只记录)——真实闸门口径
103
+ osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
104
+ tags=["preference"], shared=True)
105
+ osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
106
+ tags=["finance.transfer"], shared=True)
107
+
108
+ recs = osi.shared_write_audit.read()
109
+ check(len(recs) == 2, f"两笔共享写入都落盘:实际 {len(recs)}")
110
+ by_allowed = {r["payload"]["allowed"] for r in recs}
111
+ check(by_allowed == {True, False}, "allowed 的 True/False 都被原样记录(判读依据完整)")
112
+ denied = [r for r in recs if not r["payload"]["allowed"]]
113
+ check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
114
+ "越界那条可从文件直接判读")
115
+ # 与脊柱 history 口径一致:文件条数 == 脊柱里该主题条数
116
+ spine_n = len(osi.event_spine.history("memory.shared.write", 100))
117
+ check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
118
+ finally:
119
+ shutil.rmtree(tmp, ignore_errors=True)
120
+
121
+
122
+ def main():
123
+ print("=" * 60)
124
+ print("审计落盘 sink(P0-3 配套)")
125
+ print("=" * 60)
126
+ test_unit()
127
+ test_end_to_end()
128
+ print("\n" + "=" * 60)
129
+ if _FAILS:
130
+ print(f"✗ {len(_FAILS)}/{_N} 项失败"); return 1
131
+ print(f"✓ 全部通过({_N} 项)")
132
+ return 0
133
+
134
+
135
+ if __name__ == "__main__":
136
+ raise SystemExit(main())