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,142 @@
1
+ """失败策略表:把「工具失败了」映射成「下一步做什么」。
2
+
3
+ 病根:工具轮的失败此前只有两种归宿——原样回注给模型,或直接丢掉。于是「超时了该重试
4
+ 一次还是放弃」「没配搜索服务该降级还是换参数」全靠模型临场发挥;而模型最常见的反应是
5
+ **换个说法把没做成的事说成做成了**(静默假成功)。这张表把那类决策从模型手里拿走:
6
+ 错误特征 → 动作,查表即得,和 recover 层「由策略表决定,不猜」同一原则。
7
+
8
+ 设计取舍:
9
+ - **按文本特征分类,不按异常类型**。工具跑在守护线程里(见 agents/mixins/tool_exec),
10
+ 跨线程、跨工具之后剩下的只有一句回注文本;异常类型在这里已经丢失。
11
+ - **只有只读工具才自动重试**。写/发送/支付类动作重试一次 = 可能重复扣款、重复发消息;
12
+ 这类一律交人。
13
+ - 分类是**启发式**的,因此:匹配不到 → 当作未知失败(不重试、交人),绝不乐观当成功。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+
19
+ OK = "ok"
20
+ TIMEOUT = "timeout"
21
+ TRANSIENT = "transient"
22
+ NOT_CONFIGURED = "not_configured"
23
+ EMPTY = "empty"
24
+ INVALID_ARGS = "invalid_args"
25
+ DENIED = "denied"
26
+ NEEDS_HUMAN = "needs_human"
27
+ EXCEPTION = "exception"
28
+ UNKNOWN = "unknown"
29
+
30
+ NONE = "none"
31
+ RETRY = "retry"
32
+ REPARAM = "reparam"
33
+ DEGRADE = "degrade"
34
+ HAND_TO_HUMAN = "hand_to_human"
35
+ STOP = "stop"
36
+
37
+ _RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
38
+ (DENIED, ("已被策略禁用", "被权限策略拒绝", "未知工具")),
39
+ (NEEDS_HUMAN, ("需确认的动作", "需要你确认", "请用户确认", "只能由你亲自确认")),
40
+ (INVALID_ARGS, ("入参不合法", "缺少必填", "类型应为")),
41
+ (TIMEOUT, ("超过", "超时", "timed out", "timeout")),
42
+ (NOT_CONFIGURED, ("未配置", "not configured", "未设置", "缺少 api", "缺少 key")),
43
+ (EMPTY, ("未返回结果", "未召回到", "no result", "未命中", "没有找到")),
44
+ (TRANSIENT, ("网络", "连接失败", "connection", "502", "503", "504", "429", "rate limit")),
45
+ (EXCEPTION, ("执行异常",)),
46
+ )
47
+
48
+ _ACTIONS: dict[str, tuple[str, bool, str]] = {
49
+ TIMEOUT: (RETRY, True,
50
+ "这一步超时未完成,已自动重试一次仍有问题。请如实说明这一步没做成,不要编造结果。"),
51
+ TRANSIENT: (RETRY, True,
52
+ "这次失败像是临时的网络/服务问题,已自动重试一次。若仍失败,如实说明当前无法获取。"),
53
+ EXCEPTION: (RETRY, True,
54
+ "工具内部异常,已自动重试一次。若仍失败,如实说明该步骤未能执行。"),
55
+ EMPTY: (REPARAM, False,
56
+ "调用成功但没有拿到内容。可以换一个更具体的查询词或放宽条件再试一次;"
57
+ "仍为空就如实回答「没有查到」,不要用常识补全成确定结论。"),
58
+ INVALID_ARGS: (REPARAM, False,
59
+ "参数不合法,这一步没有执行。请按工具声明修正参数后再调用;"
60
+ "不要把失败当成已完成。"),
61
+ NOT_CONFIGURED: (DEGRADE, False,
62
+ "该能力当前未配置(服务/密钥缺失),不是暂时故障,重试无用。"
63
+ "请告知用户这项能力暂不可用,不要假装查到了。"),
64
+ NEEDS_HUMAN: (HAND_TO_HUMAN, False,
65
+ "这是需要用户确认的动作,我没有执行。把要做的操作拟成草稿交用户确认。"),
66
+ DENIED: (STOP, False, "该工具被策略禁止,不要换个名字或换个参数再试。"),
67
+ UNKNOWN: (HAND_TO_HUMAN, False,
68
+ "这一步的结果不可信(无法判定成功还是失败)。不要向用户断言它已完成。"),
69
+ OK: (NONE, False, ""),
70
+ }
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Decision:
75
+ """一次失败判定:类型 → 动作 + 给模型看的提示。"""
76
+
77
+ kind: str
78
+ action: str
79
+ retryable: bool
80
+ hint: str
81
+
82
+ @property
83
+ def is_ok(self) -> bool:
84
+ return self.kind == OK
85
+
86
+ def to_dict(self) -> dict:
87
+ return {"kind": self.kind, "action": self.action,
88
+ "retryable": self.retryable, "hint": self.hint}
89
+
90
+
91
+ _FAILURE_MARKERS = ("执行异常", "入参不合法", "被权限策略拒绝", "缺少必填")
92
+
93
+
94
+ def looks_like_failure(text: str) -> bool:
95
+ t = (text or "").strip()
96
+ if not t:
97
+ return True
98
+ return t.startswith("(") or any(m in t for m in _FAILURE_MARKERS)
99
+
100
+
101
+ def classify(text: str) -> str:
102
+ """把回注文本归类:``ok`` / 具体失败类型 / ``unknown``(认不出来,保守交人)。"""
103
+ t = (text or "").strip()
104
+ if not t:
105
+ return UNKNOWN
106
+ if not looks_like_failure(t):
107
+ return OK
108
+ low = t.lower()
109
+ for kind, needles in _RULES:
110
+ if any(n in low for n in needles):
111
+ return kind
112
+ return UNKNOWN
113
+
114
+
115
+ def decide(text: str, tool: str = "", capability: dict | None = None,
116
+ attempt: int = 1, max_attempts: int = 2) -> Decision:
117
+ """查表得到动作。
118
+
119
+ ``attempt`` 是**已尝试次数**(第一次调用传 1)。可重试类型在 ``attempt < max_attempts``
120
+ 时给 ``retry``;只读工具才真的会被重试 —— 这里只判定,是否执行由调用方决定,
121
+ 因为"这个工具能不能安全重复执行"属于能力声明(capability),不是文本语义。
122
+ """
123
+ kind = classify(text)
124
+ action, retryable, hint = _ACTIONS.get(kind, _ACTIONS[UNKNOWN])
125
+ read_only = bool((capability or {}).get("read_only"))
126
+ if action == RETRY and (not read_only or attempt >= max_attempts):
127
+ action = HAND_TO_HUMAN
128
+ if not read_only:
129
+ hint = ("这一步有副作用,重试可能重复执行,因此没有自动重试。"
130
+ "请如实说明它没有完成。")
131
+ return Decision(kind=kind, action=action, retryable=retryable, hint=hint)
132
+
133
+
134
+ def annotate(text: str, decision: Decision) -> str:
135
+ """把动作提示拼到回注文本后面(仅失败时;成功文本一个字都不改)。
136
+
137
+ 为什么要拼在文本里而不是另走一路:回注给模型的是**纯文本**,多一条通道意味着
138
+ 模型可能只读到其中一条;拼在末尾让"结果"和"该怎么对待这个结果"不可分割。
139
+ """
140
+ if decision.is_ok or not decision.hint:
141
+ return text or ""
142
+ return f"{text or ''}\n[执行策略] {decision.hint}"
@@ -0,0 +1,81 @@
1
+ """
2
+ 统一 API 网关限流(gateway.py)
3
+
4
+ 把散落在各处(MCP、/chat/stream)的单点限流收敛成一层**统一网关**:
5
+ · per-client 令牌桶——突发限流(每分钟额度,允许瞬时打满桶)
6
+ · 固定窗口配额——持续限流(每小时总额度)
7
+ · 空闲客户端淘汰——防攻击者轮换 key/IP 把 per-client 字典撑爆内存(与 metrics「unmatched」同款教训)
8
+
9
+ client 身份 = X-API-Key(有则)否则来源 IP。线程安全(单锁,check 内无 await/无阻塞 I/O)。
10
+ 默认宽松:本地单用户近乎无感;对外暴露时经环境变量收紧。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ import threading
16
+
17
+
18
+ class Gateway:
19
+ def __init__(self, rate_per_min: int = 600, quota_per_hour: int = 20000,
20
+ max_clients: int = 4096):
21
+ self.rate = max(1, int(rate_per_min))
22
+ self.capacity = float(self.rate)
23
+ self.refill = self.capacity / 60.0
24
+ self.quota = max(1, int(quota_per_hour))
25
+ self.max_clients = max(64, int(max_clients))
26
+ self._lock = threading.Lock()
27
+ self._buckets: dict = {}
28
+ self._quota: dict = {}
29
+ self._seen: dict = {}
30
+ self.rejected = 0
31
+
32
+ def check(self, client: str):
33
+ """判定一次请求。返回 (allowed: bool, retry_after: int, reason: str)。
34
+ reason ∈ {'', 'rate', 'quota'}。allowed=False 时 retry_after 为建议重试秒数。"""
35
+ now = time.time()
36
+ with self._lock:
37
+ self._evict_locked(now)
38
+ self._seen[client] = now
39
+
40
+ q = self._quota.get(client)
41
+ if q is None or now - q[1] >= 3600.0:
42
+ q = [0, now]
43
+ self._quota[client] = q
44
+ if q[0] >= self.quota:
45
+ self.rejected += 1
46
+ return False, int(3600.0 - (now - q[1])) + 1, "quota"
47
+
48
+ b = self._buckets.get(client)
49
+ if b is None:
50
+ b = [self.capacity, now]
51
+ self._buckets[client] = b
52
+ b[0] = min(self.capacity, b[0] + (now - b[1]) * self.refill)
53
+ b[1] = now
54
+ if b[0] < 1.0:
55
+ self.rejected += 1
56
+ return False, int((1.0 - b[0]) / self.refill) + 1, "rate"
57
+
58
+ b[0] -= 1.0
59
+ q[0] += 1
60
+ return True, 0, ""
61
+
62
+ def _evict_locked(self, now: float):
63
+ """超过 max_clients 时淘汰最久未见的一批(降到 ~90%)。必须在锁内调用。"""
64
+ if len(self._seen) <= self.max_clients:
65
+ return
66
+ n_drop = len(self._seen) - self.max_clients + max(1, self.max_clients // 10)
67
+ victims = sorted(self._seen.items(), key=lambda kv: kv[1])[:n_drop]
68
+ for c, _ in victims:
69
+ self._seen.pop(c, None)
70
+ self._buckets.pop(c, None)
71
+ self._quota.pop(c, None)
72
+
73
+ def stats(self) -> dict:
74
+ with self._lock:
75
+ return {
76
+ "tracked_clients": len(self._seen),
77
+ "rate_per_min": self.rate,
78
+ "quota_per_hour": self.quota,
79
+ "max_clients": self.max_clients,
80
+ "rejected_total": self.rejected,
81
+ }
@@ -0,0 +1,171 @@
1
+ """
2
+ 治理决策统一审计(P1-2 基础 / P3-4 decision_id)。
3
+
4
+ 系统的裁决点分散在四层,各说各话:
5
+ · Harness(推理漂移:语义/工具/范围)—— emit harness_pass/warn/block
6
+ · Permission(动作权限:deny>ask>allow 四阶段)—— 经 PolicyGate emit policy.decision.*
7
+ · Orchestrator(跨 Agent 委派裁决)—— 原本**无任何事件**
8
+ · SkillAdapter(执行终态:成功/失败/断路器/封杀)—— 原本**无任何事件**
9
+
10
+ 本模块提供一条**统一治理审计流**,不改变任何裁决行为,只做记录:
11
+
12
+ 事件通道:脊柱广播 `governance.decision`(不在 WS 静音列表 → 前端可见)
13
+ 落盘通道:{data_dir}/audit/governance.jsonl(环形缓冲之外的持久样本)
14
+
15
+ 每条记录带 **decision_id**(本次裁决的唯一 ID)与 **correlation_id**(跨层关联键):
16
+ · permission 裁决生成 decision_id → 同一 SkillAdapter.invoke 的执行审计用它做
17
+ correlation_id ⇒ 「谁批准的这个动作、执行结果如何」可join
18
+ · Orchestrator 用 req.id 做 correlation_id ⇒ 多轮协商的历次裁决串成一条链
19
+
20
+ verdict 词汇(两类,查询时注意区分):
21
+ · 裁决类:allow / warn / deny / confirm(与 PolicyGate 归一词汇一致)
22
+ · 执行类:success / failed(SkillAdapter 的执行终态)
23
+
24
+ 记录策略(防止高频正常路径淹没审计流):
25
+ · harness / permission 层只记**非 allow**(warn/deny/confirm)——PASS 是每步推理的常态
26
+ · orchestration 层全量(跨 Agent 委派本来就低频)
27
+ · skill 层全量(执行审计是 P2-0 的核心价值;一次任务仅个位数调用)
28
+
29
+ 与 shared_write 审计(audit_sink.py)的关系:那是「记忆写入准入」专项,这是「治理裁决」
30
+ 总流;两者并存、各自落盘,互不替代。
31
+
32
+ 开关:FOXCLAW_GOVERNANCE_AUDIT=0 关闭(默认开;每条 <500 字节,量级可忽略)。
33
+ 审计一律 fail-safe:任何记录异常都静默吞掉,绝不影响裁决主流程(与闸门同约束)。
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import json
38
+ import os
39
+ import threading
40
+ import uuid
41
+ from datetime import datetime, timezone
42
+ from pathlib import Path
43
+
44
+ from . import audit_chain
45
+
46
+ ENV_SWITCH = "FOXCLAW_GOVERNANCE_AUDIT"
47
+ TOPIC = "governance.decision"
48
+
49
+ VERDICTS_RULING = ("allow", "warn", "deny", "confirm")
50
+ VERDICTS_EXECUTION = ("success", "failed")
51
+
52
+ LAYERS = ("harness", "permission", "orchestration", "skill")
53
+
54
+
55
+ def new_decision_id() -> str:
56
+ """生成一次治理裁决的唯一 ID。短 hex 够用(单机审计,非全局唯一约束)。"""
57
+ return f"dec_{uuid.uuid4().hex[:12]}"
58
+
59
+
60
+ class GovernanceAuditor:
61
+ """统一治理审计记录器:发脊柱事件 + 可选 JSONL 落盘。
62
+
63
+ 线程安全:记录可能来自任意 Agent 的任意线程(事件脊柱 handler 在锁外执行,
64
+ 但 invoke/route_request 本身可被并发调用)——落盘用进程内锁串行。
65
+ 多进程部署(FOXCLAW_ALLOW_INSECURE 的多 worker)不在本机开发场景内,从略。
66
+ """
67
+
68
+ def __init__(self, spine=None, sink_path: Path | None = None):
69
+ self._spine = spine
70
+ self._path = Path(sink_path) if sink_path else None
71
+ self._lock = threading.Lock()
72
+ self._last_hash: str | None = None
73
+
74
+ @property
75
+ def path(self) -> Path | None:
76
+ return self._path
77
+
78
+ def record(self, *, layer: str, actor: str, action: str, verdict: str,
79
+ reason: str = "", detail: dict | None = None,
80
+ decision_id: str | None = None,
81
+ correlation_id: str | None = None) -> str:
82
+ """记录一条治理裁决/执行审计,返回 decision_id(显式传入则原样返回)。
83
+
84
+ 全参数关键字化:调用点必须写明每个字段是什么,避免位置参数错位造成
85
+ 「审计记错层」这种难以察觉的污染。
86
+ """
87
+ did = decision_id or new_decision_id()
88
+ rec = {
89
+ "decision_id": did,
90
+ "correlation_id": correlation_id or "",
91
+ "layer": layer,
92
+ "actor": actor,
93
+ "action": action,
94
+ "verdict": verdict,
95
+ "reason": (reason or "")[:200],
96
+ "detail": detail or {},
97
+ "ts": datetime.now(timezone.utc).isoformat(),
98
+ }
99
+ self._emit(rec)
100
+ self._persist(rec)
101
+ return did
102
+
103
+
104
+ def _emit(self, rec: dict) -> None:
105
+ if self._spine is None:
106
+ return
107
+ try:
108
+ self._spine.publish(TOPIC, dict(rec), source="governance_audit",
109
+ correlation_id=rec["correlation_id"] or None)
110
+ except Exception:
111
+ pass
112
+
113
+ def _persist(self, rec: dict) -> None:
114
+ """落盘时接哈希链(见 core/audit_chain.py):改一行/删中间一行都能被 verify 发现。
115
+
116
+ 事件通道仍发**不带链字段**的原始 rec —— 前端只关心裁决内容,而链是落盘证据;
117
+ 两个通道内容不同是有意的,别拿 WS 事件当真伪校验的依据。
118
+ """
119
+ if self._path is None:
120
+ return
121
+ try:
122
+ with self._lock:
123
+ self._path.parent.mkdir(parents=True, exist_ok=True)
124
+ if self._last_hash is None:
125
+ self._last_hash = audit_chain.tail_hash(self._path)
126
+ chained = audit_chain.link(rec, self._last_hash)
127
+ with open(self._path, "a", encoding="utf-8") as f:
128
+ f.write(json.dumps(chained, ensure_ascii=False) + "\n")
129
+ self._last_hash = chained["hash"]
130
+ except Exception:
131
+ pass
132
+
133
+ def read(self, limit: int = 200) -> list:
134
+ """读落盘记录(新→旧),坏行跳过。供工具/测试/未来 API 用。"""
135
+ if self._path is None or not self._path.exists():
136
+ return []
137
+ out = []
138
+ try:
139
+ with open(self._path, encoding="utf-8") as f:
140
+ for line in f:
141
+ line = line.strip()
142
+ if not line:
143
+ continue
144
+ try:
145
+ out.append(json.loads(line))
146
+ except Exception:
147
+ continue
148
+ except Exception:
149
+ return []
150
+ return out[-limit:][::-1]
151
+
152
+ def verify(self) -> tuple:
153
+ """校验落盘链 → (ok, 条数, 问题)。无落盘时视为通过。
154
+
155
+ 用法:`python -m foxclaw.core.audit_chain {data_dir}/audit/governance.jsonl`。
156
+ """
157
+ if self._path is None:
158
+ return True, 0, ""
159
+ return audit_chain.verify(self._path)
160
+
161
+
162
+ def attach(spine, data_dir) -> GovernanceAuditor | None:
163
+ """装配工厂:FOXCLAW_GOVERNANCE_AUDIT=0 时返回 None(各层按 None 跳过记录)。
164
+
165
+ 落盘到 {data_dir}/audit/governance.jsonl(该目录已被 .gitignore 覆盖)。
166
+ spine 为 None 时仍可落盘(standalone/测试场景),只是没有事件通道。
167
+ """
168
+ if os.environ.get(ENV_SWITCH, "1") == "0":
169
+ return None
170
+ sink = Path(data_dir) / "audit" / "governance.jsonl" if data_dir else None
171
+ return GovernanceAuditor(spine=spine, sink_path=sink)
@@ -0,0 +1,282 @@
1
+ """
2
+ Foxclaw Core — 幂等守卫 (Idempotency Guard)
3
+
4
+ 借鉴 OpenClaw 的幂等原理,适配 Foxclaw。核心思想:
5
+ 给每个"只应发生一次"的操作算一个稳定 key,执行前查重——
6
+ 记录过就跳过(返回上次结果),没记录才执行并落记录。
7
+
8
+ 为什么需要:消息平台天生会重发(网络抖动、平台重试、轮询游标边界),
9
+ 没有幂等,用户会收到重复回复、同一操作被记两次。
10
+
11
+ 设计要点:
12
+ · key 稳定:同一逻辑操作每次算出同一 key(版本+域+标识)
13
+ · 跨重启:可选落盘,进程重启后仍能识别"这条处理过了"
14
+ · 不膨胀:TTL 过期清理 + 容量上限(LRU 淘汰)
15
+ · 并发安全:RLock 保护
16
+ """
17
+ from __future__ import annotations
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import threading
22
+ import time
23
+ from dataclasses import dataclass, field
24
+
25
+
26
+ @dataclass
27
+ class _Record:
28
+ key: str
29
+ created: float
30
+ result: dict = field(default_factory=dict)
31
+
32
+
33
+ def make_key(domain: str, *parts: str, version: str = "v1") -> str:
34
+ """构造稳定幂等 key:version:domain:hash(parts)。
35
+
36
+ domain 区分用途(message/dispatch/reply...),parts 是业务标识
37
+ (渠道+消息ID、会话+步骤号等)。相同输入永远得到相同 key。
38
+ """
39
+ raw = "|".join(str(p) for p in parts)
40
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:20]
41
+ return f"{version}:{domain}:{digest}"
42
+
43
+
44
+ class IdempotencyGuard:
45
+ """幂等守卫。check_and_mark 是核心:原子地"查重 + 标记"。
46
+
47
+ 【P2-3】持久化从"全量快照"改为"追加日志 + 定期 compact":
48
+ · 每次操作(mark/check_and_mark/update_result/容量淘汰)只追加一行 JSONL(O(1) 写);
49
+ · 每 compact_every 次操作(或启动加载后)compact 成快照并清空日志;
50
+ · 启动时读快照 + 回放追加日志(兼容旧的纯快照格式:无日志可回放,自然兼容);
51
+ · 持久化失败不再静默:计数进 stats(),有 spine 时发 idempotency.persist_failed 事件。
52
+ """
53
+
54
+ def __init__(self, ttl_seconds: float = 3600, max_size: int = 10000,
55
+ persist_path: str | None = None, spine=None,
56
+ compact_every: int = 1000):
57
+ self.ttl = ttl_seconds
58
+ self.max_size = max_size
59
+ self.persist_path = persist_path
60
+ self._store: dict[str, _Record] = {}
61
+ self._lock = threading.RLock()
62
+ self._spine = spine
63
+ self._log_path = (persist_path + ".log") if persist_path else None
64
+ self._compact_every = compact_every
65
+ self._ops_since_compact = 0
66
+ self._persist_failures = 0
67
+ self._compact_lock = threading.Lock()
68
+ if persist_path:
69
+ self._load()
70
+
71
+ def seen(self, key: str) -> bool:
72
+ """该 key 是否已记录(且未过期)。"""
73
+ with self._lock:
74
+ self._evict_expired()
75
+ return key in self._store
76
+
77
+ def get_result(self, key: str) -> dict | None:
78
+ """取上次执行结果(用于重发时原样返回,避免重复执行副作用)。"""
79
+ with self._lock:
80
+ rec = self._store.get(key)
81
+ return dict(rec.result) if rec else None
82
+
83
+ def mark(self, key: str, result: dict | None = None):
84
+ """标记 key 已执行,记录结果。"""
85
+ with self._lock:
86
+ self._store[key] = _Record(key=key, created=time.time(),
87
+ result=dict(result or {}))
88
+ self._enforce_capacity()
89
+ self._persist_op("mark", key, created=self._store[key].created,
90
+ result=self._store[key].result)
91
+ need_compact = self._ops_since_compact >= self._compact_every
92
+ if need_compact:
93
+ self._try_compact()
94
+
95
+ def check_and_mark(self, key: str, result: dict | None = None) -> bool:
96
+ """原子操作:若 key 未见过 → 标记并返回 True(应执行);
97
+ 若已见过 → 返回 False(应跳过)。
98
+
99
+ 这是防并发重复的关键:查重和标记在同一把锁里完成,
100
+ 避免"两个重发同时查到未记录、都去执行"。
101
+ """
102
+ with self._lock:
103
+ self._evict_expired()
104
+ if key in self._store:
105
+ return False
106
+ self._store[key] = _Record(key=key, created=time.time(),
107
+ result=dict(result or {}))
108
+ self._enforce_capacity()
109
+ self._persist_op("mark", key, created=self._store[key].created,
110
+ result=self._store[key].result)
111
+ need_compact = self._ops_since_compact >= self._compact_every
112
+ if need_compact:
113
+ self._try_compact()
114
+ return True
115
+
116
+ def update_result(self, key: str, result: dict):
117
+ """补写结果(check_and_mark 先占位、执行完再回填结果时用)。"""
118
+ with self._lock:
119
+ rec = self._store.get(key)
120
+ if rec:
121
+ rec.result = dict(result or {})
122
+ self._persist_op("update", key, result=rec.result)
123
+ need_compact = self._ops_since_compact >= self._compact_every
124
+ else:
125
+ need_compact = False
126
+ if need_compact:
127
+ self._try_compact()
128
+
129
+ def _evict_expired(self):
130
+ if self.ttl <= 0:
131
+ return
132
+ now = time.time()
133
+ expired = [k for k, r in self._store.items() if now - r.created > self.ttl]
134
+ for k in expired:
135
+ del self._store[k]
136
+
137
+ def _enforce_capacity(self):
138
+ if len(self._store) <= self.max_size:
139
+ return
140
+ items = sorted(self._store.items(), key=lambda kv: kv[1].created)
141
+ for k, _ in items[: len(self._store) - self.max_size]:
142
+ del self._store[k]
143
+ self._persist_op("delete", k)
144
+
145
+ def _persist_op(self, event: str, key: str, **fields):
146
+ """【P2-3】追加一条日志(O(1) 写,不写全量)。锁内调用(append 很快,
147
+ 且在锁内可避免并发写同一文件交错)。失败计数 + 发事件,不影响主流程。"""
148
+ if not self._log_path:
149
+ return
150
+ try:
151
+ entry = {"event": event, "key": key, "ts": time.time(), **fields}
152
+ with open(self._log_path, "a", encoding="utf-8") as f:
153
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
154
+ self._ops_since_compact += 1
155
+ except Exception:
156
+ self._persist_failures += 1
157
+ self._emit_persist_failed()
158
+
159
+ def _emit_persist_failed(self):
160
+ """【P2-3】持久化失败发事件(有 spine 时),失败本身绝不再抛。"""
161
+ if self._spine is None:
162
+ return
163
+ try:
164
+ self._spine.publish("idempotency.persist_failed", {
165
+ "failures": self._persist_failures,
166
+ "log_path": self._log_path,
167
+ }, source="idempotency")
168
+ except Exception:
169
+ pass
170
+
171
+ def _try_compact(self):
172
+ """【P2-3】触发 compact,**全量写盘在主锁外**:
173
+ 主锁内只做两件快事——日志轮转(rename,新 append 自动开新文件)和内存快照拷贝;
174
+ 慢的全量 JSON 写盘在锁外做,不阻塞并发 mark/check。
175
+ 独立 _compact_lock 非阻塞抢占:抢不到说明另一线程正在 compact,直接跳过。
176
+ 正确性:轮转后新 append 进新 .log,写快照+删轮转本不影响它们,无丢失窗口。"""
177
+ if not self.persist_path:
178
+ return
179
+ if not self._compact_lock.acquire(blocking=False):
180
+ return
181
+ try:
182
+ with self._lock:
183
+ rotated = None
184
+ if self._log_path and os.path.exists(self._log_path):
185
+ rotated = self._log_path + ".rot"
186
+ os.replace(self._log_path, rotated)
187
+ now = time.time()
188
+ snapshot = {k: {"created": r.created, "result": r.result}
189
+ for k, r in self._store.items()
190
+ if self.ttl <= 0 or now - r.created <= self.ttl}
191
+ self._ops_since_compact = 0
192
+ tmp = self.persist_path + ".tmp"
193
+ with open(tmp, "w", encoding="utf-8") as f:
194
+ json.dump(snapshot, f, ensure_ascii=False)
195
+ os.replace(tmp, self.persist_path)
196
+ if rotated and os.path.exists(rotated):
197
+ os.remove(rotated)
198
+ except Exception:
199
+ with self._lock:
200
+ self._persist_failures += 1
201
+ self._emit_persist_failed()
202
+ finally:
203
+ self._compact_lock.release()
204
+
205
+ def _compact(self):
206
+ """启动加载后的同步 compact(单线程上下文,直接全量写)。"""
207
+ if not self.persist_path:
208
+ return
209
+ try:
210
+ now = time.time()
211
+ data = {k: {"created": r.created, "result": r.result}
212
+ for k, r in self._store.items()
213
+ if self.ttl <= 0 or now - r.created <= self.ttl}
214
+ tmp = self.persist_path + ".tmp"
215
+ with open(tmp, "w", encoding="utf-8") as f:
216
+ json.dump(data, f, ensure_ascii=False)
217
+ os.replace(tmp, self.persist_path)
218
+ if self._log_path and os.path.exists(self._log_path):
219
+ os.remove(self._log_path)
220
+ self._ops_since_compact = 0
221
+ except Exception:
222
+ self._persist_failures += 1
223
+ self._emit_persist_failed()
224
+
225
+ def _apply_log_event(self, ev: dict, now: float):
226
+ """回放一条追加日志事件。"""
227
+ kind = ev.get("event")
228
+ key = ev.get("key")
229
+ if not key:
230
+ return
231
+ if kind == "mark":
232
+ created = ev.get("created", now)
233
+ if self.ttl <= 0 or now - created <= self.ttl:
234
+ self._store[key] = _Record(key=key, created=created,
235
+ result=ev.get("result", {}))
236
+ elif kind == "update":
237
+ rec = self._store.get(key)
238
+ if rec:
239
+ rec.result = ev.get("result", {})
240
+ elif kind == "delete":
241
+ self._store.pop(key, None)
242
+
243
+ def _load(self):
244
+ """读全量快照 + 回放追加日志。兼容旧的纯快照格式(无日志,回放为空)。
245
+ 仅当真有日志被回放时才 compact 一次(合并进快照)——否则每次构造都白写
246
+ 一份全量快照,失去"追加日志 O(1)"的意义。"""
247
+ try:
248
+ now = time.time()
249
+ snapshot = self.persist_path
250
+ if snapshot is not None and os.path.exists(snapshot):
251
+ with open(snapshot, encoding="utf-8") as f:
252
+ data = json.load(f)
253
+ for k, v in data.items():
254
+ created = v.get("created", now)
255
+ if self.ttl <= 0 or now - created <= self.ttl:
256
+ self._store[k] = _Record(key=k, created=created,
257
+ result=v.get("result", {}))
258
+ replayed = False
259
+ if self._log_path and os.path.exists(self._log_path):
260
+ with open(self._log_path, encoding="utf-8") as f:
261
+ for line in f:
262
+ line = line.strip()
263
+ if not line:
264
+ continue
265
+ try:
266
+ ev = json.loads(line)
267
+ except Exception:
268
+ continue
269
+ self._apply_log_event(ev, now)
270
+ replayed = True
271
+ if replayed:
272
+ self._compact()
273
+ except Exception:
274
+ pass
275
+
276
+ def stats(self) -> dict:
277
+ with self._lock:
278
+ self._evict_expired()
279
+ return {"size": len(self._store), "max_size": self.max_size,
280
+ "ttl_seconds": self.ttl, "persisted": bool(self.persist_path),
281
+ "persist_failures": self._persist_failures,
282
+ "ops_since_compact": self._ops_since_compact}