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,430 @@
1
+ """环境变量配置:类型化读取 + 启动前校验 + 可操作提示。
2
+
3
+ ## 为什么需要这个模块
4
+
5
+ 配置散落在 20 多个文件、135 处 `os.environ.get(...)` 里。缺项时大多有默认值(不崩),
6
+ 但**类型写错会崩在很远的地方**,而且崩得毫无线索:
7
+
8
+ FOXCLAW_PORT=80oo
9
+ → ValueError: invalid literal for int() with base 10: '80oo'
10
+ File ".../foxclaw/api/middleware.py", line 173, in <module>
11
+
12
+ 用户看到的是 `middleware.py` 第 173 行 —— 但问题在他自己的 `.env` 第 12 行,而且真正
13
+ 想改的可能是 `FOXCLAW_PORT` 之外的另一处。另一种更常见的坏情况是**根本不崩**:
14
+ `FOXCLAW_TOOL_TIMEOUTS=60`(多打一个 S)永远不会报错,只是那个设置悄悄不生效。
15
+
16
+ 所以这里做三件事:
17
+
18
+ 1. **类型化读取**(`get_int` / `get_float`):解析失败时抛 `ConfigError`,消息里带
19
+ 「变量名 + 你写的原值 + 期望格式 + 示例 + 是哪个文件第几行在读它」。
20
+ 2. **`check()` 一次性汇总所有问题**:不 fail-fast,一次把该改的都列出来;区分 error
21
+ (会崩/意图破)与 warn(可疑但能跑)。
22
+ 3. **未知键探测**:扫描包内源码,收集真正被读取的变量名;`.env` 里出现同前缀但没人读
23
+ 的键 → 大概率拼错,给出最接近的正确名(difflib)。
24
+
25
+ ## 刻意不做的事
26
+
27
+ - **不统一布尔语义**。现有各处写法不一致且都有意为之:
28
+ `url_policy` 是 `in ("1","true","yes")`(默认关),`web_search` 是
29
+ `not in ("0","false","False","")`(默认开)。强行统一 = 静默改变已有行为,
30
+ 这类"顺手的重构"是回归温床,不动。
31
+ - **不改任何默认值、不接管 `os.environ`**。`get_*` 是纯读函数,不写回环境、不做缓存
32
+ (运行期改环境变量仍然即时生效,与现状一致)。
33
+ - **不把配置抽成全局 settings 对象**。那需要改动所有调用点,收益只是"好看",
34
+ 风险却是整条启动链路。
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import ast
39
+ import inspect
40
+ import logging
41
+ import os
42
+ from dataclasses import dataclass
43
+ from difflib import get_close_matches
44
+ from functools import lru_cache
45
+ from pathlib import Path
46
+ from typing import Any, Iterable, Mapping, Sequence
47
+
48
+ # provider → 需要的环境变量(与 core/model_router.PROVIDER_ENV 保持一致;
49
+ # 这里重复一份是为了让 settings 不依赖 model_router —— 那会拖入 LLM SDK 的导入)。
50
+ PROVIDER_KEY_ENV: dict[str, str] = {
51
+ "anthropic": "ANTHROPIC_API_KEY",
52
+ "openai": "OPENAI_API_KEY",
53
+ "deepseek": "DEEPSEEK_API_KEY",
54
+ "google": "GOOGLE_API_KEY",
55
+ "qwen": "DASHSCOPE_API_KEY",
56
+ "local": "LOCAL_LLM_BASE",
57
+ "minimax": "MINIMAX_API_KEY",
58
+ "generic": "LLM_API_KEY",
59
+ }
60
+
61
+ # 只检查这些前缀下的"未知键":它们属于本项目或本项目专用的厂商约定,
62
+ # 出现没人读的名字几乎必然是拼错。像 `PATH` / `HOME` 这类系统变量不在此列。
63
+ OWNED_PREFIXES = (
64
+ "FOXCLAW_", "BAIDU_", "AMAP_", "LLM_", "GENERIC_LLM_",
65
+ "MINIMAX_", "LOCAL_LLM_", "HKC_", "HMR_",
66
+ )
67
+
68
+
69
+ class ConfigError(ValueError):
70
+ """某个环境变量的值不合法。消息本身就是给用户看的修复指引。"""
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Opt:
75
+ """一个环境变量的声明。kind 只描述**解析方式**,不代表业务语义。"""
76
+
77
+ name: str
78
+ kind: str = "str" # str | int | float | csv | choice
79
+ default: Any = ""
80
+ group: str = "其他"
81
+ hint: str = "" # 期望格式 / 示例(拼进出错信息)
82
+ choices: Sequence[str] = ()
83
+ secret: bool = False
84
+ lo: float | None = None
85
+ hi: float | None = None
86
+
87
+
88
+ SPECS: tuple[Opt, ...] = (
89
+ # lo/hi 只给**调用点自己不兜底**的项。像 TRACE_RETAIN 已有 max(100, ...)、
90
+ # BAIDU_SEARCH_TIMEOUT 已有 max(5, ...):在那里再报一次范围错,等于把"clamp"
91
+ # 变成"整条配置作废退默认",反而更糟。
92
+ # ── 模型 ────────────────────────────────────────────────────────────────
93
+ Opt("LLM_PROVIDER", "choice", "", "模型",
94
+ "anthropic / openai / deepseek / google / qwen / local / minimax / generic",
95
+ tuple(PROVIDER_KEY_ENV)),
96
+ Opt("FOXCLAW_PROVIDER", "choice", "", "模型", "旧名,同上(优先用 LLM_PROVIDER)",
97
+ tuple(PROVIDER_KEY_ENV)),
98
+ Opt("LLM_MODEL", "str", "", "模型", "具体模型名,留空用该 provider 的默认模型"),
99
+ Opt("FOXCLAW_MODEL", "str", "", "模型", "已弃用,改用 LLM_MODEL"),
100
+ Opt("FOXCLAW_LLM_COOLDOWN", "float", 20.0, "模型",
101
+ "秒,某 provider 失败后的冷却基准", lo=0),
102
+ Opt("DEEPSEEK_API_KEY", "str", "", "模型", "DeepSeek 的 key", secret=True),
103
+ Opt("ANTHROPIC_API_KEY", "str", "", "模型", "Anthropic 的 key", secret=True),
104
+ Opt("OPENAI_API_KEY", "str", "", "模型", "OpenAI 的 key", secret=True),
105
+ Opt("GOOGLE_API_KEY", "str", "", "模型", "Google Gemini 的 key", secret=True),
106
+ Opt("DASHSCOPE_API_KEY", "str", "", "模型", "阿里云百炼(通义)的 key", secret=True),
107
+ Opt("MINIMAX_API_KEY", "str", "", "模型", "MiniMax 的 key", secret=True),
108
+ Opt("MINIMAX_GROUP_ID", "str", "", "模型", "MiniMax 的 group id(与 key 配对)"),
109
+ Opt("LLM_API_KEY", "str", "", "模型", "generic(通用 openai 兼容)的 key", secret=True),
110
+ Opt("LLM_BASE_URL", "str", "", "模型", "generic 的 base_url,如 https://host/v1"),
111
+ Opt("LOCAL_LLM_BASE", "str", "", "模型", "本地推理服务的 base_url"),
112
+ # ── 服务 ────────────────────────────────────────────────────────────────
113
+ Opt("FOXCLAW_PORT", "int", 8000, "服务", "HTTP 监听端口", lo=1, hi=65535),
114
+ Opt("FOXCLAW_DATA_DIR", "str", "", "服务", "数据目录(默认 <仓库>/foxclaw_data)"),
115
+ Opt("FOXCLAW_IMPORT_ROOTS", "csv", "", "服务",
116
+ "额外允许的导入目录(逗号分隔);不配则只有数据目录内的文件可被导入"),
117
+ Opt("FOXCLAW_API_KEY", "str", "", "服务",
118
+ "对外访问口令;设了才启用鉴权(空 = 仅本机开放)", secret=True),
119
+ Opt("FOXCLAW_CORS_ORIGINS", "str", "", "服务",
120
+ "逗号分隔的来源;留空只放行回环,\"*\" 全开(危险)"),
121
+ Opt("FOXCLAW_ALLOWED_HOSTS", "csv", "", "服务", "逗号分隔的 Host 白名单"),
122
+ Opt("FOXCLAW_GATEWAY", "choice", "", "服务", "1 强制开网关限流 / 0 强制关",
123
+ ("0", "1")),
124
+ Opt("FOXCLAW_GATEWAY_RATE", "int", 600, "服务", "每客户端每分钟请求数", lo=0),
125
+ Opt("FOXCLAW_GATEWAY_QUOTA", "int", 20000, "服务", "每客户端每小时配额", lo=0),
126
+ Opt("FOXCLAW_MAX_BODY_MB", "int", 32, "服务", "请求体上限(MB)", lo=1),
127
+ Opt("FOXCLAW_STREAM_RATE", "int", 60, "服务", "流式端点每分钟上限", lo=0),
128
+ Opt("FOXCLAW_STREAM_CONCURRENCY", "int", 4, "服务", "流式并发上限", lo=1),
129
+ Opt("FOXCLAW_SHADOW_ROUNDS", "int", 20, "服务", "影子推演轮数", lo=1),
130
+ # ── 工具 / 追踪 ─────────────────────────────────────────────────────────
131
+ Opt("FOXCLAW_TOOL_TIMEOUT", "float", 45.0, "工具",
132
+ "单个工具调用硬超时(秒);可小于 1(测试用),调用点会兜 >0"),
133
+ Opt("FOXCLAW_TRACE_SLOW_MS", "float", 1000.0, "追踪", "超过多少毫秒算慢请求", lo=0),
134
+ Opt("FOXCLAW_TRACE_SAMPLE", "float", 0.0, "追踪",
135
+ "非慢请求的采样率 0~1;超出范围由调用点 clamp,不在这里报错"),
136
+ Opt("FOXCLAW_TRACE_RETAIN", "int", 5000, "追踪",
137
+ "内存里保留的 trace 条数;调用点已有 max(100, ...) 兜底"),
138
+ # ── 搜索 ────────────────────────────────────────────────────────────────
139
+ Opt("BAIDU_SEARCH_KEY", "str", "", "搜索", "百度 AI 搜索的 key", secret=True),
140
+ Opt("BAIDU_SEARCH_MODEL", "str", "", "搜索", "百度搜索用的模型名"),
141
+ Opt("BAIDU_SEARCH_BASE", "str", "", "搜索", "百度搜索的 base_url"),
142
+ Opt("BAIDU_SEARCH_TIMEOUT", "int", 45, "搜索", "秒;调用点已有 max(5, ...) 兜底"),
143
+ # ── 渠道 ────────────────────────────────────────────────────────────────
144
+ Opt("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "csv", "", "渠道",
145
+ "逗号分隔的可信发送者:<id> / <channel>:<id> / <channel>:*"),
146
+ Opt("FOXCLAW_CHANNEL_TRUST_ALL", "str", "", "渠道",
147
+ "1/true 才让裸 `*` 生效(全局信任,不推荐)"),
148
+ Opt("FOXCLAW_CHANNEL_SIGNING_SECRET", "str", "", "渠道",
149
+ "出站 HMAC 密钥;留空 = 不签", secret=True),
150
+ Opt("FOXCLAW_CHANNEL_SIGNING_KEY_ID", "str", "", "渠道", "密钥轮换标识"),
151
+ # ── 浏览器 ──────────────────────────────────────────────────────────────
152
+ Opt("FOXCLAW_BROWSER_ALLOW_PRIVATE", "str", "", "浏览器",
153
+ "1/true/yes 才允许访问内网地址"),
154
+ )
155
+
156
+ SPEC_BY_NAME: dict[str, Opt] = {o.name: o for o in SPECS}
157
+
158
+ # 会在启动/运行早期被读、且类型写错就会崩的少数几个 —— 供 serve 预检快速拦截。
159
+ FATAL_CODES = ("type", "choice", "range")
160
+
161
+
162
+ @dataclass
163
+ class Issue:
164
+ level: str # "error" | "warn"
165
+ code: str # type / choice / range / provider_key / unknown_key ...
166
+ name: str
167
+ message: str
168
+ hint: str = "" # 修复建议(可为空)
169
+
170
+ def __str__(self) -> str:
171
+ # 只用 ASCII 记号:Windows 控制台默认 GBK,✓/✗/⚠ 会抛 UnicodeEncodeError
172
+ mark = "ERROR" if self.level == "error" else " WARN"
173
+ head = f"[{mark}] {self.name}: {self.message}" if self.name else f"[{mark}] {self.message}"
174
+ return f"{head}\n → {self.hint}" if self.hint else head
175
+
176
+
177
+ def _caller() -> str:
178
+ """当前 `get_*` 的调用点(文件:行)。只在出错路径上求值,不进热路径。"""
179
+ try:
180
+ f = inspect.currentframe()
181
+ for _ in range(3): # 跳过 get_* → _parse → _caller 自身
182
+ if f is None:
183
+ return ""
184
+ f = f.f_back
185
+ if f is None:
186
+ return ""
187
+ return f"{Path(f.f_code.co_filename).name}:{f.f_lineno}"
188
+ except Exception: # noqa: BLE001 诊断信息不值得再抛一次
189
+ return ""
190
+
191
+
192
+ def _parse(opt: Opt, raw: str, where: str = "") -> Any:
193
+ """把原始字符串转成目标类型;不合法抛 ConfigError(消息可直接给用户看)。"""
194
+ text = raw.strip()
195
+ expect = opt.hint or opt.kind
196
+ suffix = f"(错误位置:{where})" if where else ""
197
+
198
+ if opt.kind in ("int", "float"):
199
+ try:
200
+ value = int(text) if opt.kind == "int" else float(text)
201
+ except ValueError:
202
+ raise ConfigError(
203
+ f'值 "{raw}" 不是{"整数" if opt.kind == "int" else "数字"}。'
204
+ f"期望:{expect};例如 {opt.name}="
205
+ f'{opt.default if opt.default != "" else 1}{suffix}') from None
206
+ if opt.lo is not None and value < opt.lo:
207
+ raise ConfigError(f"值 {value} 小于下限 {opt.lo}(期望:{expect}){suffix}")
208
+ if opt.hi is not None and value > opt.hi:
209
+ raise ConfigError(f"值 {value} 大于上限 {opt.hi}(期望:{expect}){suffix}")
210
+ return value
211
+
212
+ if opt.kind == "choice":
213
+ value = text.lower()
214
+ if value not in opt.choices:
215
+ raise ConfigError(
216
+ f'值 "{raw}" 不在允许范围内。可选:{" / ".join(opt.choices)}{suffix}')
217
+ return value
218
+
219
+ if opt.kind == "csv":
220
+ return [s.strip() for s in text.split(",") if s.strip()]
221
+
222
+ return raw # str:保留原样(尾部空格可能有意义,如口令)
223
+
224
+
225
+ def _opt(name: str) -> Opt:
226
+ return SPEC_BY_NAME.get(name) or Opt(name, "str", "")
227
+
228
+
229
+ def get(name: str) -> Any:
230
+ """读取并按声明解析。未设置或空串 → 返回默认值(空串视为未设置)。
231
+
232
+ 为什么把空串当未设置:`.env` 里 `FOXCLAW_PORT=` 这种写法今天会让
233
+ `int("")` 抛 ValueError 崩掉,而用户的本意显然是"用默认值"。
234
+ """
235
+ opt = _opt(name)
236
+ raw = os.environ.get(name)
237
+ if raw is None or raw.strip() == "":
238
+ return opt.default
239
+ return _parse(opt, raw, _caller())
240
+
241
+
242
+ def get_int(name: str) -> int:
243
+ return int(get(name))
244
+
245
+
246
+ def get_float(name: str) -> float:
247
+ return float(get(name))
248
+
249
+
250
+ _warned: set[str] = set()
251
+ _log = logging.getLogger("foxclaw.config")
252
+
253
+
254
+ def get_safe(name: str, default: Any = None) -> Any:
255
+ """可降级的读取:值非法时**退默认值并要求日志警告一次**,不抛异常。
256
+
257
+ 给"配置错不该拖垮主流程"的地方用(trace 采样、工具超时、搜索超时):这些点
258
+ 原来就 try/except 静默退默认,问题是用户永远不知道自己的设置没生效。改成
259
+ "仍然退默认,但警告一次并说清哪个变量、错在哪"。
260
+ """
261
+ opt = _opt(name)
262
+ fallback = opt.default if default is None else default
263
+ raw = os.environ.get(name)
264
+ if raw is None or not raw.strip():
265
+ return fallback # 未设置/空串:用调用方给的兜底
266
+ try:
267
+ return _parse(opt, raw, _caller())
268
+ except ConfigError as e:
269
+ if name not in _warned:
270
+ _warned.add(name) # 只喊一次:tracing 是每请求调用
271
+ _log.warning("[config] %s", e)
272
+ return fallback
273
+
274
+
275
+ def mask(value: str) -> str:
276
+ """密钥脱敏:只留前 2 后 2,中间用等长星号(长度也藏掉,免得泄露口令强度)。"""
277
+ v = str(value or "")
278
+ if not v:
279
+ return "(空)"
280
+ if len(v) <= 4:
281
+ return "*" * len(v)
282
+ return f"{v[:2]}{'*' * 6}{v[-2:]}"
283
+
284
+
285
+ def is_secret(name: str) -> bool:
286
+ opt = SPEC_BY_NAME.get(name)
287
+ if opt is not None and opt.secret:
288
+ return True
289
+ return any(t in name for t in ("KEY", "TOKEN", "SECRET", "PASSWORD", "SCODE"))
290
+
291
+
292
+ @lru_cache(maxsize=1)
293
+ def known_keys() -> frozenset[str]:
294
+ """包内源码**实际读取**的环境变量名(静态扫 AST,排除 vendor/)。
295
+
296
+ 比手写清单可靠:新增 `os.environ.get("X")` 就自动算"已知",不需要谁记得来同步。
297
+ 只在 `check()` / `show()` 这类诊断路径调用,不进请求路径;结果缓存在本进程内。
298
+ """
299
+ pkg = Path(__file__).resolve().parent.parent
300
+ # vendor/(HKC/HMR)在仓库根,不在包内:它们自己读 HMR_*/HKC_* 变量,不一起扫就会把
301
+ # 正常配置报成"拼错的键"。wheel 里没有 vendor/ → 扫描自然为空,不影响安装后的行为。
302
+ roots = [pkg] + ([pkg.parent / "vendor"] if (pkg.parent / "vendor").is_dir() else [])
303
+ found: set[str] = set(SPEC_BY_NAME)
304
+ files: list[Path] = []
305
+ for root in roots:
306
+ files += [x for x in sorted(root.rglob("*.py"))
307
+ if "__pycache__" not in x.as_posix() and "tests/" not in x.as_posix()]
308
+ for path in files:
309
+ try:
310
+ tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
311
+ except SyntaxError: # 语法坏掉的文件留着给 CI 报,这里跳过
312
+ continue
313
+ for node in ast.walk(tree):
314
+ if isinstance(node, ast.Call):
315
+ fn = ast.unparse(node.func)
316
+ if fn in ("os.environ.get", "os.getenv", "environ.get", "getenv"):
317
+ arg = node.args[0] if node.args else None
318
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
319
+ found.add(arg.value)
320
+ elif isinstance(node, ast.Subscript) and ast.unparse(node.value) in (
321
+ "os.environ", "environ"):
322
+ if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):
323
+ found.add(node.slice.value)
324
+ return frozenset(found)
325
+
326
+
327
+ def _provider_issues(env: Mapping[str, str]) -> list[Issue]:
328
+ raw = (env.get("LLM_PROVIDER") or env.get("FOXCLAW_PROVIDER") or "").strip().lower()
329
+ if raw and raw in PROVIDER_KEY_ENV:
330
+ need = PROVIDER_KEY_ENV[raw]
331
+ if not (env.get(need) or "").strip():
332
+ return [Issue(
333
+ "error", "provider_key", need,
334
+ f"已指定 LLM_PROVIDER={raw},但这个 provider 需要的 {need} 是空的 → "
335
+ f"启动后不会真的调用模型,只会静默退化成 stub 演示回复",
336
+ f"补上 {need},或把 LLM_PROVIDER 改成已配好 key 的 provider,或删掉 LLM_PROVIDER 让它自动探测")]
337
+ if not any((env.get(k) or "").strip() for k in PROVIDER_KEY_ENV.values()):
338
+ return [Issue(
339
+ "warn", "no_llm", "",
340
+ "没有任何模型 key(ANTHROPIC/OPENAI/DEEPSEEK/GOOGLE/DASHSCOPE/MINIMAX/LLM_API_KEY 都空)"
341
+ " → 以 stub 演示模式运行",
342
+ "本地试用可以忽略;要真实回答,至少配一个 *_API_KEY")]
343
+ return []
344
+
345
+
346
+ def _consistency_issues(env: Mapping[str, str]) -> list[Issue]:
347
+ out: list[Issue] = []
348
+ key_id = (env.get("FOXCLAW_CHANNEL_SIGNING_KEY_ID") or "").strip()
349
+ secret = (env.get("FOXCLAW_CHANNEL_SIGNING_SECRET") or "").strip()
350
+ if key_id and not secret:
351
+ out.append(Issue("warn", "signing_pair", "FOXCLAW_CHANNEL_SIGNING_KEY_ID",
352
+ "配了 key_id 但没有 FOXCLAW_CHANNEL_SIGNING_SECRET"
353
+ " → 签名不会启用,key_id 被忽略",
354
+ "要么补上 SECRET,要么把 KEY_ID 也删掉"))
355
+ if 0 < len(secret) < 16:
356
+ out.append(Issue("warn", "weak_secret", "FOXCLAW_CHANNEL_SIGNING_SECRET",
357
+ f"密钥只有 {len(secret)} 个字符,HMAC 强度不足",
358
+ "至少 32 字节随机值,如 `openssl rand -hex 32` 的输出"))
359
+ if (env.get("FOXCLAW_CORS_ORIGINS") or "").strip() == "*" and not (
360
+ env.get("FOXCLAW_API_KEY") or "").strip():
361
+ out.append(Issue("warn", "open_cors", "FOXCLAW_CORS_ORIGINS",
362
+ "CORS 全开(\"*\")且没设 FOXCLAW_API_KEY → 任意网页都能调本机 API",
363
+ "本地开发建议改成 http://localhost:5173 这种具体来源;"
364
+ "确实要跨源就在设 API_KEY 之后再说"))
365
+ if (env.get("FOXCLAW_GATEWAY") or "").strip() == "0" and (
366
+ env.get("FOXCLAW_API_KEY") or "").strip():
367
+ out.append(Issue("warn", "gateway_off", "FOXCLAW_GATEWAY",
368
+ "对外开了鉴权(配了 API_KEY)却强制关掉网关限流"
369
+ " → 拿到口令的人可以不限速地打",
370
+ "去掉 FOXCLAW_GATEWAY=0(网关默认会随 API_KEY 自动开启)"))
371
+ return out
372
+
373
+
374
+ def _unknown_key_issues(env: Mapping[str, str]) -> list[Issue]:
375
+ known = known_keys()
376
+ out: list[Issue] = []
377
+ for name in sorted(env):
378
+ if name in known or not name.startswith(OWNED_PREFIXES):
379
+ continue
380
+ near = get_close_matches(name, sorted(known), n=1, cutoff=0.75)
381
+ out.append(Issue(
382
+ "warn", "unknown_key", name,
383
+ "这个变量没有任何代码读取,设了也不会生效(多半是拼错)",
384
+ f"你是想写 {near[0]} 吗?" if near else "删掉它,或对照 .env.example 检查拼写"))
385
+ return out
386
+
387
+
388
+ def check(env: Mapping[str, str] | None = None) -> list[Issue]:
389
+ """一次汇总全部配置问题(不抛异常、不 fail-fast)。
390
+
391
+ `env` 可注入(dict),便于测试与"拿一份 .env 先试探"。
392
+ """
393
+ env = os.environ if env is None else env
394
+ issues: list[Issue] = []
395
+ for opt in SPECS:
396
+ raw = env.get(opt.name)
397
+ if raw is None or raw.strip() == "":
398
+ continue
399
+ try:
400
+ _parse(opt, raw, where="")
401
+ except ConfigError as e:
402
+ code = "choice" if opt.kind == "choice" else (
403
+ "range" if ("下限" in str(e) or "上限" in str(e)) else "type")
404
+ # 密钥不回显原值,免得把半截口令打进日志
405
+ msg = str(e)
406
+ if is_secret(opt.name):
407
+ msg = f"值 {mask(raw)} 不合法(原始值已脱敏)"
408
+ issues.append(Issue("error", code, opt.name, msg,
409
+ "" if (opt.hint and opt.hint in msg) else opt.hint))
410
+ issues += _provider_issues(env)
411
+ issues += _consistency_issues(env)
412
+ issues += _unknown_key_issues(env)
413
+ return issues
414
+
415
+
416
+ def format_issues(issues: Iterable[Issue], limit: int = 20) -> str:
417
+ items = list(issues)
418
+ if not items:
419
+ return "配置检查通过:没有发现问题。"
420
+ errors = sum(1 for i in items if i.level == "error")
421
+ lines = [f"配置检查:{errors} 个错误 / {len(items) - errors} 个提示"]
422
+ lines += [str(i) for i in items[:limit]]
423
+ if len(items) > limit:
424
+ lines.append(f"... 另有 {len(items) - limit} 条(用 --json 看全部)")
425
+ return "\n".join(lines)
426
+
427
+
428
+ def fatal_issues(issues: Iterable[Issue]) -> list[Issue]:
429
+ """会在启动早期直接崩掉的那类(类型/取值非法)。网关/模型这类语义问题不算。"""
430
+ return [i for i in issues if i.level == "error" and i.code in FATAL_CODES]
@@ -0,0 +1,198 @@
1
+ """
2
+ 共享层写入策略 (SharedWritePolicy) —— P0-3
3
+
4
+ 问题
5
+ ────
6
+ memory_adapter.ingest(..., shared=True) 此前**没有任何权限校验**。而共享层是所有 Agent
7
+ 都能召回的记忆区 —— 于是「谁能往共享层写什么」等价于「谁能给所有 Agent 留话」。
8
+
9
+ 一个具体危害:办公 Agent 可以写一条「财务应给办公转账」进共享层,之后财务 Agent 在召回
10
+ 时读到它。这是一条**完全绕开守门人**的跨 Agent 注入通道 —— 守门人管的是"动作",而共享层
11
+ 写的是"下一轮推理的上下文"。
12
+
13
+ 本模块只做一件事:把「谁写了什么到共享层」变成**可判定、可观测**的事实。
14
+
15
+ 三种模式(FOXCLAW_SHARED_WRITE_MODE)
16
+ ──────────────────────────────────
17
+ audit (默认) 只记录,不拦截。写入照常落库,事件里标 allowed=True/False。
18
+ warn 记录 + 进程内告警,仍不拦截。
19
+ enforce 真正拦截:未被策略允许的写入抛 SharedWriteDenied。
20
+
21
+ 为什么默认 audit
22
+ ────────────────
23
+ 现有代码有 8 处**合法**共享写入(主管分发记录 6 处、各 Agent 的用户偏好共享 1 处、
24
+ 真值管线沉淀 1 处)。策略表在真实流量上跑通之前是一份**待验证的假设**,不是既成事实 ——
25
+ 贸然 enforce 会误伤正常链路。先在 audit 下看清越界写入长什么样,再切 enforce。
26
+ (这也是文稿 P0-3 的建议:三模式、默认灰度。)
27
+
28
+ 范围说明(诚实标注)
29
+ ──────────────────
30
+ 本模块**不改变召回侧的隔离**。召回仍由 memory_adapter 的 tag 过滤实现(`agent:<id>` +
31
+ `shared`),那是"标签约定"而非结构性隔离 —— 属另一议题,不在本次范围。
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import fnmatch
37
+ import hashlib
38
+ import os
39
+ from dataclasses import dataclass
40
+ from typing import Iterable, Optional
41
+
42
+ MODE_ENV = "FOXCLAW_SHARED_WRITE_MODE"
43
+ VALID_MODES = ("audit", "warn", "enforce")
44
+
45
+ # 与写入方无关、任何 Agent 都可写的 facet(设计上就该跨 Agent 共享的东西):
46
+ # preference —— 用户偏好:base.py 明确以 shared=True 写入,意在跨会话跨 Agent 生效
47
+ # truth_pipeline —— 真值管线沉淀(truth_pipeline.py),写入方是 agent_id="system"
48
+ GLOBAL_ALLOWED_FACETS = frozenset({"preference", "truth_pipeline"})
49
+
50
+ # 各 Agent 的「职责域」允许 facet。facet 取自一次写入的 {type:<记忆类型>} ∪ tags,
51
+ # 支持 fnmatch 通配(如 finance.* 可匹配 finance.budget)。
52
+ #
53
+ # 注意:这是**起始假设**,不是已验证的结论 —— 默认 audit 模式的目的正是用真实流量验证它。
54
+ DEFAULT_SHARED_WRITE_ALLOWED = {
55
+ "supervisor": {"*"}, # 主管:分发/协调记录,语义上允许任意
56
+ "system": {"*"}, # 系统(真值管线、后台任务)
57
+ "finance": {"finance.*"},
58
+ # 行政并入办公后,办公学习 Agent 同时拥有学习与行程两个写域;
59
+ # 助手已下放为技能(不再有独立 agent_id),不再单列 ACL 条目。
60
+ "office": {"learning.*", "itinerary.*", "schedule.*"},
61
+ }
62
+
63
+
64
+ class SharedWriteDenied(PermissionError):
65
+ """共享层写入被策略拒绝(enforce 模式下抛出)。"""
66
+
67
+ def __init__(self, agent_id: str, reason: str, content: str = ""):
68
+ self.agent_id = agent_id
69
+ self.reason = reason
70
+ self.content = content
71
+ super().__init__(f"共享层写入被拒 agent={agent_id!r}: {reason}")
72
+
73
+
74
+ @dataclass
75
+ class Decision:
76
+ """一次共享层写入的判定结果。"""
77
+ allowed: bool
78
+ reason: str
79
+ mode: str
80
+ facets: tuple = ()
81
+
82
+ def to_dict(self) -> dict:
83
+ return {"allowed": self.allowed, "reason": self.reason,
84
+ "mode": self.mode, "facets": list(self.facets)}
85
+
86
+
87
+ class SharedWritePolicy:
88
+ """判定「某 Agent 能否把某条内容写入共享层」。
89
+
90
+ 无状态、无锁、纯函数式判定 —— 并发安全直接成立,也便于单独测试。
91
+ """
92
+
93
+ def __init__(self, allowed: dict = None, mode: str = None,
94
+ global_facets: Iterable[str] = None):
95
+ self.allowed = {k: set(v) for k, v in
96
+ (allowed or DEFAULT_SHARED_WRITE_ALLOWED).items()}
97
+ self.global_facets = set(global_facets if global_facets is not None
98
+ else GLOBAL_ALLOWED_FACETS)
99
+ self.mode = self._resolve_mode(mode)
100
+
101
+ @staticmethod
102
+ def _resolve_mode(mode: str = None) -> str:
103
+ """显式入参 > 环境变量 > 默认 audit。非法值一律回落到 audit(保守:不拦截)。"""
104
+ m = (mode or os.environ.get(MODE_ENV, "") or "audit").strip().lower()
105
+ return m if m in VALID_MODES else "audit"
106
+
107
+ # 纯标记类 tag:描述"这条记忆的归属/去向",不描述"内容是什么"。
108
+ # 让它们参与内容域匹配只会稀释策略表 —— 尤其 "shared" 在本闸门语境下恒为真,零信息量。
109
+ _MARKER_TAGS = ("agent:", "shared")
110
+
111
+ @staticmethod
112
+ def facets_of(memory_type: str, tags: Iterable[str]) -> tuple:
113
+ """一次写入的 facet 集合:{type:<记忆类型>} ∪ 原样 tag(去重保序)。
114
+
115
+ `agent:<id>` 与 `shared` 被刻意排除 —— 它们是**隔离/去向标记**,描述"谁的、
116
+ 写给谁看的",不是"什么内容",拿它们参与内容域匹配只会让策略表变得没有意义。
117
+ """
118
+ out = []
119
+ if memory_type:
120
+ out.append(f"type:{memory_type}")
121
+ for t in (tags or ()):
122
+ if not isinstance(t, str) or not t:
123
+ continue
124
+ if t == "shared" or t.startswith("agent:"):
125
+ continue
126
+ out.append(t)
127
+ return tuple(dict.fromkeys(out))
128
+
129
+ def evaluate(self, agent_id: str = None, memory_type: str = "concept",
130
+ tags: Iterable[str] = None) -> Decision:
131
+ facets = self.facets_of(memory_type, tags)
132
+ who = agent_id or "unknown"
133
+
134
+ if any(fnmatch.fnmatchcase(f, p) for f in facets for p in self.global_facets):
135
+ return Decision(True, "命中全局允许 facet", self.mode, facets)
136
+
137
+ patterns = self.allowed.get(who)
138
+ if patterns is None:
139
+ return Decision(False, f"未登记的写入方 {who!r}", self.mode, facets)
140
+ if any(fnmatch.fnmatchcase(f, p) for f in facets for p in patterns):
141
+ return Decision(True, f"{who} 的职责域允许", self.mode, facets)
142
+ return Decision(False, f"{who} 的职责域不含 {list(facets)}", self.mode, facets)
143
+
144
+
145
+ def guard_shared_write(backend, agent_id: str, content: str,
146
+ memory_type: str = "concept",
147
+ tags: Iterable[str] = None) -> Optional[Decision]:
148
+ """共享层写入的统一闸门 —— 必须在**真正落库之前**调用。
149
+
150
+ 行为分三档:
151
+ · 未挂策略(独立使用适配层 / 老调用方)→ 直接返回 None,保持完全向后兼容。
152
+ · audit / warn → 发 memory.shared.write 审计事件,不拦截。
153
+ · enforce → 未允许则抛 SharedWriteDenied。
154
+
155
+ 设计约束:审计**绝不能**影响写入本身 —— 事件失败被吞掉,只影响可观测性,不影响记忆落库。
156
+ """
157
+ policy = getattr(backend, "shared_write_policy", None)
158
+ if policy is None:
159
+ return None
160
+
161
+ decision = policy.evaluate(agent_id, memory_type, tags)
162
+
163
+ spine = getattr(backend, "shared_write_spine", None)
164
+ if spine is not None:
165
+ try:
166
+ spine.publish("memory.shared.write", {
167
+ "agent_id": agent_id or "",
168
+ "type": memory_type,
169
+ "tags": [t for t in (tags or ()) if isinstance(t, str)],
170
+ "allowed": decision.allowed,
171
+ "reason": decision.reason,
172
+ "mode": decision.mode,
173
+ # 只留摘要哈希 + 短预览,不把共享层正文原样灌进事件流
174
+ "content_sha": hashlib.sha256(
175
+ str(content or "").encode("utf-8")).hexdigest()[:16],
176
+ "content_preview": str(content or "")[:60],
177
+ }, source=agent_id or "memory")
178
+ except Exception:
179
+ pass
180
+
181
+ if not decision.allowed:
182
+ if policy.mode == "enforce":
183
+ raise SharedWriteDenied(agent_id or "unknown", decision.reason, content)
184
+ if policy.mode == "warn":
185
+ print(f"[Foxclaw] ⚠ 共享层越界写入(未拦截): agent={agent_id!r} "
186
+ f"reason={decision.reason}")
187
+ return decision
188
+
189
+
190
+ def attach(backend, policy: SharedWritePolicy = None, spine=None) -> SharedWritePolicy:
191
+ """把策略挂到记忆后端(HMRAdapter / MockHMR 皆可)。返回生效的策略实例。
192
+
193
+ 用挂载而非构造函数注入,是因为装配顺序:memory_adapter 在 event_spine **之前**创建,
194
+ 审计出口只有等脊柱建好才能挂上(见 foxclaw/__init__.py 的装配顺序)。
195
+ """
196
+ backend.shared_write_policy = policy or SharedWritePolicy()
197
+ backend.shared_write_spine = spine
198
+ return backend.shared_write_policy