foxclaw 0.1.2 → 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 (344) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -3,43 +3,31 @@ Foxclaw API — FastAPI 后端
3
3
  前端控制台通过这里获取所有数据
4
4
  """
5
5
 
6
+ import threading
6
7
  import os
7
8
  import asyncio
8
9
  import queue
9
10
  from pathlib import Path
10
11
  from contextlib import asynccontextmanager
11
12
 
12
- # 加载项目根目录的 .env(若存在),让所有 Key 从配置文件读取。
13
- #
14
- # 【FOXCLAW_SKIP_DOTENV】跳过 .env,只用进程环境变量 —— 专供测试/CI 使用。
15
- # 为什么必须有这个逃生口:load_dotenv(override=False) 只保护「进程里已存在的键」。
16
- # 测试若先 pop 掉某键(如 test_gateway.py pop FOXCLAW_API_KEY 以验证「未设即开放」),
17
- # 再 import 本模块,.env 就会把它灌回来 → 测试实际跑在开发者的真实配置上。
18
- # 后果有三:(1) 同一份代码在不同机器上结果不同,回归不可复现;
19
- # (2) .env 里的真实 Key 会让测试真的打到外部 API(产生费用);
20
- # (3) FOXCLAW_DATA_DIR=./foxclaw_data 会让测试写进用户的真实数据目录。
21
- # 设 FOXCLAW_SKIP_DOTENV=1 后,进程环境变量成为唯一来源,测试环境完全自洽。
22
13
  try:
23
14
  from ..core.envfile import load_env_file
24
15
  _env_path = Path(__file__).parent.parent.parent / ".env"
25
16
  if os.environ.get("FOXCLAW_SKIP_DOTENV", "").strip() in ("1", "true", "True", "yes"):
26
17
  print("[Foxclaw] ○ FOXCLAW_SKIP_DOTENV=1 —— 跳过 .env,仅用进程环境变量")
27
18
  elif _env_path.exists():
28
- # 非空值优先:系统环境变量里的同名旧 key 不再压住 .env(见 core/envfile.py)
29
19
  _shadows = load_env_file(_env_path)
30
20
  print("[Foxclaw] ✓ 已加载配置文件 .env")
31
21
  if _shadows:
32
22
  print("[Foxclaw] ! .env 覆盖了系统环境变量: " + ", ".join(_shadows))
33
23
  except ImportError:
34
- pass # 未装 python-dotenv 则用系统环境变量
24
+ pass
35
25
 
36
26
  from fastapi import FastAPI
37
27
  from typing import Optional
38
28
 
39
29
  from foxclaw import Foxclaw, __version__
40
30
 
41
- # 中间件实现已移至 middleware.py;此处保留符号作 re-export(测试/外部经 foxclaw.api 访问),
42
- # 冗余别名 `X as X` 让 ruff 识别为显式 re-export(F401 硬门)。install_middleware 在下方调用。
43
31
  from .middleware import (
44
32
  install_middleware,
45
33
  _constant_time_eq as _constant_time_eq,
@@ -55,23 +43,15 @@ from .middleware import (
55
43
 
56
44
  from .routes.ws import manager, harness_websocket as harness_websocket
57
45
 
58
- # 事件脊柱辅助(WS 净化 / HKC 预热)已移至 spine.py;`_register_spine_projection` 因
59
- # `_WS_MUTED` 源码级断言约束仍留本文件(见 test_governance_audit)。
60
46
  from .spine import _ws_safe_payload, _prewarm_hkc
61
- # app 挂载/配置(CORS / HKC 星球数据面 / 前端静态)已移至 app.py,在下方按原顺序调用。
62
- from .app import _configure_cors, _mount_hkc, _mount_frontend
47
+ from ._state import current_os, set_os_instance
48
+ from ._state import os_instance as os_instance
63
49
 
50
+ from .bootstrap import _configure_cors, _mount_hkc, _mount_frontend
64
51
 
65
- # ── 全局 OS 实例 ───────────────────────────────────────────
66
- _os_instance: Optional[Foxclaw] = None
67
-
68
- # 线程安全事件队列:harness 在任意线程触发的事件先入队,
69
- # 由后台 async 任务统一广播到 WebSocket(解决跨线程 create_task 失败问题)
70
52
  _event_queue: "queue.Queue" = queue.Queue()
71
53
  _broadcast_task: Optional[asyncio.Task] = None
72
- # HKC 后台预热守护线程:关闭 lifespan 前必须 join,否则 aclose() 关闭 SQLite 连接时
73
- # 预热线程仍在 build_index() → use-after-free → Windows access violation(偶发崩溃)。
74
- _prewarm_thread = None
54
+ _prewarm_thread: "threading.Thread | None" = None
75
55
 
76
56
 
77
57
  def _register_spine_projection(os_instance: Foxclaw):
@@ -80,10 +60,6 @@ def _register_spine_projection(os_instance: Foxclaw):
80
60
  truth/process/memory 等系统事件被漏掉)。现在 harness 事件与系统事件同经脊柱,
81
61
  一个投影全量观测。payload 经 _ws_safe_payload 净化(JSON 安全 + 有界),避免脏/大
82
62
  对象丢连接或泛洪。event/data 形状与旧版一致(前端不受影响,未知事件被忽略)。"""
83
- # 高频事件:给重放/审计用(仍在脊柱 history + robot sink),不广播到 WS 以免泛洪。
84
- # · agent.cognition.*:逐 stage 认知事件
85
- # · knowledge.created/updated:HKC 批量摄入一本书→数百条,量大;merged/conflict/
86
- # ability/agent.used_ability 等"有意义"的知识事件仍广播(低频、可观测知识演化)。
87
63
  _WS_MUTED = ("knowledge.created", "knowledge.updated")
88
64
 
89
65
  def project(evt):
@@ -99,32 +75,25 @@ def _register_spine_projection(os_instance: Foxclaw):
99
75
 
100
76
  @asynccontextmanager
101
77
  async def lifespan(app: FastAPI):
102
- global _os_instance, _broadcast_task
103
- # 启动自检:未设 FOXCLAW_API_KEY 即等于无鉴权。此处只做提醒(绑定地址由启动方式决定,
104
- # 拿不到 host);__main__ 入口对「非回环 + 无鉴权」有硬拦截。
78
+ global _broadcast_task
105
79
  if not os.environ.get("FOXCLAW_API_KEY", "").strip():
106
80
  print("[Foxclaw API] ⚠ 未设置 FOXCLAW_API_KEY —— 当前无鉴权,"
107
81
  "仅本地回环安全;对外暴露前务必设置。")
108
- _os_instance = Foxclaw(
82
+ os_inst = Foxclaw(
109
83
  data_dir=os.environ.get("FOXCLAW_DATA_DIR", "./foxclaw_data"),
110
84
  shadow_rounds=settings.get_int("FOXCLAW_SHADOW_ROUNDS"),
111
85
  )
112
- _register_spine_projection(_os_instance)
113
- # trace 落库(与其它 SQLite 同放 data_dir):内存环只留最近若干条且重启即失,
114
- # 落库后才能"查昨天那次慢请求"。只留慢的/出错的 + 采样,见 tracing._should_persist。
86
+ set_os_instance(os_inst)
87
+ _register_spine_projection(os_inst)
115
88
  try:
116
89
  from ..core.tracing import TRACES
117
- TRACES.attach_db(Path(_os_instance.data_dir) / "traces.db")
90
+ TRACES.attach_db(Path(os_inst.data_dir) / "traces.db")
118
91
  except Exception as e:
119
92
  print(f"[Foxclaw API] ⚠ trace 持久化未启用: {e}")
120
- # 后台任务:把队列里的事件广播到所有 WebSocket
121
93
  _broadcast_task = asyncio.create_task(_drain_event_queue())
122
94
  print("[Foxclaw API] ✓ 事件广播任务已启动")
123
- # 预热 HKC:开机后台加载知识核心(装配 + HKC 多语言 embedding 模型 + 搜索索引 over 全量 KU),
124
- # 免得"重启后第一次知识查询"触发冷装配卡 ~1 分钟。后台守护线程跑、不阻塞启动;失败静默。
125
- _prewarm_hkc(_os_instance)
95
+ _prewarm_hkc(os_inst)
126
96
  if _foxclaw_mcp is not None:
127
- # 内置 MCP 服务器的 session_manager 需在 app lifespan 内 run()
128
97
  async with _foxclaw_mcp.session_manager.run():
129
98
  print("[Foxclaw API] ✓ 内置 MCP 服务器已挂载 /api/mcp/")
130
99
  yield
@@ -136,13 +105,11 @@ async def lifespan(app: FastAPI):
136
105
  await _broadcast_task
137
106
  except asyncio.CancelledError:
138
107
  pass
139
- # 释放持有原生资源的子系统(浏览器专用线程/会话),避免进程无法干净退出。
140
- # 先 join HKC 预热线程:预热在后台线程对 SQLite 建索引,若不 join 就直接 aclose()
141
- # 关闭 SQLite 连接,会与预热线程产生 use-after-free → Windows access violation。
142
108
  if _prewarm_thread is not None and _prewarm_thread.is_alive():
143
109
  _prewarm_thread.join(timeout=120.0)
144
- if _os_instance is not None:
145
- await _os_instance.aclose()
110
+ live = current_os()
111
+ if live is not None:
112
+ await live.aclose()
146
113
  print("[Foxclaw API] 关闭中...")
147
114
 
148
115
 
@@ -152,8 +119,6 @@ async def _drain_event_queue():
152
119
  loop = asyncio.get_running_loop()
153
120
  while True:
154
121
  try:
155
- # 带超时地阻塞取队列:超时让线程周期性醒来,
156
- # 以便被 cancel 时 executor 线程能干净退出(否则进程无法结束)
157
122
  try:
158
123
  event = await loop.run_in_executor(
159
124
  None, lambda: _event_queue.get(timeout=0.5))
@@ -166,14 +131,12 @@ async def _drain_event_queue():
166
131
  await asyncio.sleep(0.1)
167
132
 
168
133
 
169
- # ── 内置 MCP 服务器(默认关;FOXCLAW_MCP_SERVER=1 开)──────────
170
- # 把只读知识+记忆暴露成 MCP 工具,挂 /api/mcp/(自动受 X-API-Key 中间件保护)。
171
134
  _foxclaw_mcp = None
172
135
  _foxclaw_mcp_app = None
173
136
  if os.environ.get("FOXCLAW_MCP_SERVER", "0").strip() in ("1", "true", "True"):
174
137
  from ..mcp_server import build_foxclaw_mcp
175
138
  _mcp_agents = os.environ.get("FOXCLAW_MCP_AGENTS", "0").strip() in ("1", "true", "True")
176
- _foxclaw_mcp = build_foxclaw_mcp(lambda: _os_instance, expose_agents=_mcp_agents)
139
+ _foxclaw_mcp = build_foxclaw_mcp(lambda: current_os(), expose_agents=_mcp_agents)
177
140
  _foxclaw_mcp_app = _foxclaw_mcp.streamable_http_app()
178
141
  if _mcp_agents:
179
142
  print("[Foxclaw API] ⚠ MCP 已暴露 Agent 调度(ask_foxclaw)——外部客户端可驱动你的认知 Agent")
@@ -183,86 +146,64 @@ if os.environ.get("FOXCLAW_MCP_SERVER", "0").strip() in ("1", "true", "True"):
183
146
  if os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip() == "*":
184
147
  print("[Foxclaw API] ⚠ MCP 已开启但 CORS=* —— 生产建议用 FOXCLAW_CORS_ORIGINS 收紧允许来源")
185
148
 
186
- app = FastAPI(
149
+ app: FastAPI = FastAPI(
187
150
  title="Foxclaw API",
188
151
  version=__version__,
189
152
  description="Cognitive Agent Operating System",
190
153
  lifespan=lifespan,
191
154
  )
192
155
  if _foxclaw_mcp_app is not None:
193
- app.mount("/api/mcp", _foxclaw_mcp_app) # 受 _api_key_guard 保护(startswith /api)
156
+ app.mount("/api/mcp", _foxclaw_mcp_app)
194
157
 
195
- # ── CORS 收紧:实现已移至 app.py ───────────────────────────
196
158
  _configure_cors(app)
197
159
 
198
160
 
199
- # ── 中间件注册(鉴权/网关限流/追踪/指标):实现已移至 middleware.py,这里按原顺序集中挂载 ──
200
161
  install_middleware(app)
201
162
 
202
- # ── WebSocket 路由:实现已移至 routes/ws.py ────────────────
203
163
  from .routes import ws as _ws
204
164
  _ws.register(app)
205
165
 
206
- # ── 系统/监控/追踪路由:实现已移至 routes/system.py(import 即注册) ────────────────
207
166
  from .routes import system # noqa: F401
208
167
 
209
- # ── AHS 执行链路由:实现已移至 routes/ahs.py(import 即注册) ────────────────
210
168
  from .routes import ahs # noqa: F401
211
169
 
212
- # ── 记忆/会话/偏好:routes/memory.py(import 即注册) ────────────────
213
170
  from .routes import memory # noqa: F401
214
171
 
215
- # ── 任务执行/流式直答/流式任务:routes/task.py(import 即注册) ────────────────
216
172
  from .routes import task # noqa: F401
217
173
 
218
- # ── 领域内核(财务/学习/行程):routes/domain.py(import 即注册) ────────────────
219
174
  from .routes import domain # noqa: F401
220
175
 
221
- # ── 断点续跑现场:routes/recovery.py(import 即注册) ────────────────
222
176
  from .routes import recovery # noqa: F401
223
177
 
224
- # ── 主管编排:routes/orchestration.py(import 即注册) ────────────────
225
178
  from .routes import orchestration # noqa: F401
226
179
 
227
- # ── 浏览器子系统:routes/browser.py(import 即注册) ────────────────
228
180
  from .routes import browser # noqa: F401
229
181
 
230
- # ── 知识核心(HKC):routes/knowledge.py(import 即注册) ────────────────
231
182
  from .routes import knowledge # noqa: F401
232
183
 
233
- # ── SM-2 复习面:routes/review.py(import 即注册) ────────────────
234
184
  from .routes import review # noqa: F401
235
185
 
236
- # ── 认知搜索(HSR):routes/search.py(import 即注册) ────────────────
237
186
  from .routes import search # noqa: F401
238
187
 
239
- # ── 聊天附件上传/下载(多模态渲染·步骤7):routes/attachments.py(import 即注册) ────────────────
240
188
  from .routes import attachments # noqa: F401
241
189
 
242
- # ── 前端 LLM 管理(下拉式选模型 + 自定义):routes/llm_admin.py(import 即注册) ──────
243
190
  from .routes import llm_admin # noqa: F401
244
191
 
245
- # 搬移前这些是 api 模块级符号,测试/外部仍经 foxclaw.api 访问 → re-export
246
192
  from .routes.task import _sanitize_images, _sanitize_attachments, _MAX_IMAGES, _MAX_IMG_B64, _STREAM_LIMITER # noqa: F401
247
193
  from .routes.domain import _parse_import_rows # noqa: F401
248
194
  from .routes.knowledge import knowledge_ability as knowledge_ability, _HKCInsuffCov as _HKCInsuffCov # noqa: F401
249
195
  from ..core import settings
250
196
 
251
197
 
252
- # ── HKC 星球数据面:实现已移至 app.py ─────────────────────
253
198
  _mount_hkc(app)
254
199
 
255
200
 
256
- # ── 前端静态文件:实现已移至 app.py ─────────────────────────
257
201
  _mount_frontend(app)
258
202
 
259
203
 
260
204
  if __name__ == "__main__":
261
205
  import uvicorn
262
206
 
263
- # 默认只绑回环地址(本地开发)。对外暴露必须显式设 FOXCLAW_HOST,且应同时设置 FOXCLAW_API_KEY——
264
- # 否则该端口上任何可达者都能读写记忆、执行技能、摄入文件(这些端点全部无鉴权)。
265
- # 用 `python -m uvicorn foxclaw.api:app --host ...` 启动时会绕过本检查,故 lifespan 另有软提醒。
266
207
  _host = os.environ.get("FOXCLAW_HOST", "127.0.0.1").strip() or "127.0.0.1"
267
208
  _port = settings.get_int("FOXCLAW_PORT")
268
209
  _LOOPBACK = {"127.0.0.1", "localhost", "::1", "[::1]"}
@@ -0,0 +1,43 @@
1
+ """API 层共享 OS 实例的状态与访问器。
2
+
3
+ 为什么独立成模块
4
+ ----------------
5
+ `foxclaw/api/__init__.py` 受 15KB 单文件体积门(tools/size_check.py)约束,装配逻辑
6
+ 已贴着上限。把「状态 + 访问器」放这里,既让 13 个路由文件能统一取实例,也不让主装配
7
+ 文件继续膨胀。
8
+
9
+ 读写契约
10
+ --------
11
+ 写入只有 `set_os_instance()`(lifespan 启动时调用一次);读取两个入口:
12
+
13
+ - `os_instance()`:要求实例必须就绪,未就绪抛 503(可重试),用于正常请求路径
14
+ - `current_os()`:允许未就绪返回 None,用于「无 OS 也要降级返回」的只读路径
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, Optional
19
+
20
+ from fastapi import HTTPException
21
+
22
+ if TYPE_CHECKING:
23
+ from foxclaw import Foxclaw
24
+
25
+ _os_instance: Optional["Foxclaw"] = None
26
+
27
+
28
+ def set_os_instance(inst: "Foxclaw") -> None:
29
+ global _os_instance
30
+ _os_instance = inst
31
+
32
+
33
+ def current_os() -> Optional["Foxclaw"]:
34
+ return _os_instance
35
+
36
+
37
+ def os_instance() -> "Foxclaw":
38
+ """取共享 OS 实例;未就绪时抛 503 而不是让调用方吃到 NoneType 属性错误。"""
39
+ inst = _os_instance
40
+ if inst is None:
41
+ raise HTTPException(
42
+ status_code=503, detail="service is starting up, retry shortly")
43
+ return inst
@@ -15,14 +15,6 @@ import foxclaw.api as _api
15
15
 
16
16
 
17
17
  def _configure_cors(app):
18
- # ── 可选鉴权 + CORS 收紧(部署安全)──────────────────────────
19
- # FOXCLAW_API_KEY 未设 → 开放(本地默认,保持现状,既有测试/前端不受影响)。
20
- # 设了 → 所有 /api/* 请求必须带正确的 X-API-Key,否则 401。
21
- # 注意:http 中间件**看不到 websocket scope**,故 /ws 的鉴权在 WS 端点内单独做
22
- # (见 harness_websocket,用 ?key= 查询参数)。前端(静态)与 /docs 始终开放。
23
- # 默认只放行回环来源(本地开发足够:前端与 API 是同源挂载,同源请求本就不经 CORS)。
24
- # 需要跨源时设 FOXCLAW_CORS_ORIGINS=来源1,来源2(逗号分隔);显式设 "*" 才会全开(不推荐,
25
- # 与未鉴权叠加等于把 API 交给任意网页)。收紧默认值属于「默认安全」:不再默认 allow_origins=["*"]。
26
18
  _cors_env = os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip()
27
19
  if _cors_env == "*":
28
20
  _CORS_ORIGINS = ["*"]
@@ -44,12 +36,6 @@ def _configure_cors(app):
44
36
 
45
37
 
46
38
  def _mount_hkc(app):
47
- # ── HKC 星球数据面:挂载 HKC 自带的 /knowledge REST 路由 ──────────────────
48
- # 星球 UI(frontend/planet)需要 /knowledge/graph、/ku/{id}、/ku/{id}/neighbors、
49
- # /ingest/*、/reset 等。HKC 在本进程装配,这里把它自带的 knowledge 路由挂上,并用
50
- # dependency_overrides 把 get_container 指向 **Foxclaw 已装配的同一个容器**(依赖倒置,
51
- # 不另开一套;保持惰性:首个 /knowledge 请求才触发 HKC 装配)。鉴权由上面中间件覆盖 /knowledge。
52
- # 缺 vendor/hkc 或导入失败 → 静默跳过(星球无数据但不影响主应用)。
53
39
  try:
54
40
  from foxclaw.knowledge import _ensure_hkc_importable as _ensure_hkc
55
41
  _ensure_hkc()
@@ -57,33 +43,24 @@ def _mount_hkc(app):
57
43
  from hkc_api.container import get_container as _hkc_get_container
58
44
 
59
45
  def _resolve_hkc_container():
60
- svc = _api._os_instance.hkc if _api._os_instance is not None else None
46
+ os_inst = _api.current_os()
47
+ svc = os_inst.hkc if os_inst is not None else None
61
48
  if svc is None or not getattr(svc, "available", False):
62
49
  raise HTTPException(503, "HKC 未启用(未 vendor 或缺依赖)")
63
50
  return svc.container
64
51
 
65
52
  app.dependency_overrides[_hkc_get_container] = _resolve_hkc_container
66
53
  app.include_router(_hkc_knowledge_router)
67
- # /stats 系统统计:独立 hkc-api 在 main.py 顶层 @app.get("/stats") 定义,不在任何
68
- # router 里,嵌入式挂载时会漏掉 → 星球 UI 的 updateDataInfo 调 /stats 404,
69
- # 「数据目录 / 全库知识总数」提示失效。这里补挂,复用同一容器 override(惰性装配)。
70
54
  @app.get("/stats", tags=["system"])
71
55
  def _hkc_stats():
72
56
  return _resolve_hkc_container().stats()
73
- # 连接设置(星球 UI 左下的「连接设置」面板)POST /config/llm 做运行时 LLM 配置,
74
- # 覆盖 .env 的 HKC_LLM_*。独立 hkc-api 有这个路由,嵌入式 foxclaw 之前没挂 → 前端
75
- # 报「模型配置失败:404」且改配置无效。这里补挂 config 路由(同一容器 override 已覆盖),
76
- # 让「连接设置」的「知识抽取模型」段在 foxclaw 下真正生效。
77
57
  from hkc_api.routes.config import router as _hkc_config_router
78
- app.include_router(_hkc_config_router) # /config/llm(GET 读配置 / POST 改配置)
79
- # 星球副面板(冲突裁决/能力编译/最短路径)用的 HKC 路由。不挂的话 /conflicts、
80
- # /abilities、/search/path 在 Foxclaw 下 404 → 星球「冲突裁决」面板拉不到数据、
81
- # 永远显示 0(误导),能力/路径面板同样空。这里补挂(同一容器 override 已覆盖)。
58
+ app.include_router(_hkc_config_router)
82
59
  try:
83
60
  from hkc_api.routes.ability import router as _hkc_ability_router
84
61
  from hkc_api.routes.search import router as _hkc_search_router
85
- app.include_router(_hkc_ability_router) # /abilities, /conflicts
86
- app.include_router(_hkc_search_router) # /search/path 等
62
+ app.include_router(_hkc_ability_router)
63
+ app.include_router(_hkc_search_router)
87
64
  _sub = "/abilities, /conflicts, /search/*"
88
65
  except Exception as _e2:
89
66
  _sub = f"(能力/搜索路由未挂: {_e2})"
@@ -93,10 +70,6 @@ def _mount_hkc(app):
93
70
 
94
71
 
95
72
  def _mount_frontend(app):
96
- # ── 前端静态文件 ───────────────────────────────────────────
97
- # frontend/ 下现在并存三套:frontend/v3(Vue,当前主界面)、deck_v2.html(旧主界面)、
98
- # deck.html(最早的一版)。根路径必须给 v3 —— 之前一直指向 deck_v2,新版只能手输 /v3/,
99
- # 等于新界面根本没上线。
100
73
  frontend_dir = Path(__file__).parent.parent.parent / "frontend"
101
74
  if not frontend_dir.exists():
102
75
  return
@@ -60,7 +60,7 @@ def _loopback_bypass_on() -> bool:
60
60
  return False
61
61
  if v == "1":
62
62
  return True
63
- return True # 默认开
63
+ return True
64
64
 
65
65
 
66
66
  def _host_header_allowed(host_header: str) -> bool:
@@ -80,8 +80,8 @@ def _host_header_allowed(host_header: str) -> bool:
80
80
  """
81
81
  raw = (host_header or "").strip().lower()
82
82
  if not raw:
83
- return True # 无 Host(HTTP/1.0 或测试直连)→ 不在此拦
84
- if raw.startswith("["): # [::1]:8000
83
+ return True
84
+ if raw.startswith("["):
85
85
  hostname = raw.split("]")[0].lstrip("[")
86
86
  else:
87
87
  hostname = raw.rsplit(":", 1)[0] if raw.count(":") == 1 else raw
@@ -90,7 +90,7 @@ def _host_header_allowed(host_header: str) -> bool:
90
90
  return True
91
91
 
92
92
  import ipaddress
93
- try: # IP 字面量:只放行回环/私网
93
+ try:
94
94
  ip = ipaddress.ip_address(hostname)
95
95
  except ValueError:
96
96
  ip = None
@@ -104,7 +104,7 @@ def _host_header_allowed(host_header: str) -> bool:
104
104
  return True
105
105
  if hostname == pat:
106
106
  return True
107
- return "." not in hostname # 单标签主机名(localhost/testserver/机器名)放行
107
+ return "." not in hostname
108
108
 
109
109
 
110
110
  def _is_guarded_path(path: str) -> bool:
@@ -131,8 +131,6 @@ async def _api_key_guard(request, call_next):
131
131
  校验 X-API-Key。详见 _loopback_bypass_on。"""
132
132
  api_key = os.environ.get("FOXCLAW_API_KEY", "").strip()
133
133
  path = request.url.path
134
- # 【P3-rebind】与是否配 key 无关:本机开发(无 key、回环监听)才是 DNS rebinding
135
- # 的典型受害场景,所以这层放在 api_key 分支之外,默认部署也生效。
136
134
  if _is_guarded_path(path):
137
135
  _client = request.client.host if request.client else ""
138
136
  if _is_loopback_client(_client) and not _host_header_allowed(
@@ -142,21 +140,13 @@ async def _api_key_guard(request, call_next):
142
140
  {"error": "forbidden: Host 头不是本机地址(已启用 DNS rebinding 防护)。"
143
141
  "自建域名请加入 FOXCLAW_ALLOWED_HOSTS。"}, status_code=403)
144
142
  if api_key:
145
- # /knowledge/* 是 HKC 星球的数据面(graph/ingest/reset),必须同 /api 一起鉴权,
146
- # 否则 ingest/reset/graph 对公众开放(泄密+可乱写)。/ws 由端点内部鉴权。
147
143
  if _is_guarded_path(path):
148
- # CORS 预检(OPTIONS + Access-Control-Request-Method)按规范**不携带**自定义请求头,
149
- # 浏览器也不会给它带 X-API-Key。必须放行交给 CORSMiddleware 处理;否则一旦启用鉴权,
150
- # 跨源请求会在预检阶段被 401 拦死(且响应无 CORS 头,浏览器只报 CORS 错误,极难定位)。
151
144
  if request.method == "OPTIONS" and request.headers.get("access-control-request-method"):
152
145
  return await call_next(request)
153
- # 【P3-loopback】本机回环自动放行:与 docs 哲学「回环地址安全」对齐,
154
- # 避免本地开发时浏览器填 key 的体验摩擦。设 FOXCLAW_LOOPBACK_BYPASS=0 可关。
155
146
  if _loopback_bypass_on():
156
147
  client_host = request.client.host if request.client else ""
157
148
  if _is_loopback_client(client_host):
158
149
  return await call_next(request)
159
- # 【P3-1】常量时间比对,防对 API Key 的计时攻击(MCP 暴露后更重要)
160
150
  if not _constant_time_eq(request.headers.get("X-API-Key", "") or "", api_key):
161
151
  from fastapi.responses import JSONResponse
162
152
  return JSONResponse(
@@ -164,13 +154,10 @@ async def _api_key_guard(request, call_next):
164
154
  return await call_next(request)
165
155
 
166
156
 
167
- # ── 统一 API 网关:per-client 限流 + 配额(把散落的单点限流收敛成一层)──────────
168
157
  _GATEWAY = Gateway(
169
158
  rate_per_min=settings.get_int("FOXCLAW_GATEWAY_RATE"),
170
159
  quota_per_hour=settings.get_int("FOXCLAW_GATEWAY_QUOTA"),
171
160
  )
172
- # 请求体硬顶(始终生效,不依赖限流开关):防超大 body 在解析/sanitize 前就吃满内存。
173
- # 最大合法体是文件摄入(路由内 20MB 上限),故 32MB 顶对正常流量无感。
174
161
  _MAX_BODY = settings.get_int("FOXCLAW_MAX_BODY_MB") * 1024 * 1024
175
162
 
176
163
 
@@ -220,21 +207,16 @@ async def _gateway_mw(request, call_next):
220
207
  超限 → 429 + Retry-After。默认关(本地);配鉴权或显式开启才生效。"""
221
208
  path = request.url.path
222
209
  guarded = _is_guarded_path(path)
223
- # ① 请求体大小硬顶(始终生效):Content-Length 超限 → 413,在读 body 前拦下
224
210
  if guarded:
225
211
  cl = request.headers.get("content-length")
226
212
  if cl and cl.isdigit() and int(cl) > _MAX_BODY:
227
213
  from fastapi.responses import JSONResponse
228
214
  return JSONResponse(
229
215
  {"error": f"payload too large (>{_MAX_BODY // 1048576}MB)"}, status_code=413)
230
- # chunked / 无 Content-Length:靠流上累计兜底(头检查只是快路径)
231
216
  _wrap_body_limit(request, _MAX_BODY)
232
- # ② per-client 限流 + 配额(网关启用时)
233
217
  if guarded and _gateway_on():
234
218
  valid = os.environ.get("FOXCLAW_API_KEY", "").strip()
235
219
  hdr = (request.headers.get("X-API-Key") or "").strip()
236
- # 【P3-1】常量时间比较:普通 == 的短路耗时可被用来逐位猜 key,
237
- # 把请求从 ip:* 伪造成 k:* 身份绕过 IP 限流
238
220
  client = ("k:" + hdr) if (valid and _constant_time_eq(hdr, valid)) \
239
221
  else ("ip:" + (request.client.host if request.client else "anon"))
240
222
  allowed, retry, reason = _GATEWAY.check(client)
@@ -258,8 +240,6 @@ async def _trace_mw(request, call_next):
258
240
  path = request.url.path
259
241
  if not _is_guarded_path(path):
260
242
  return await call_next(request)
261
- # 跨进程接链:外部(如经 /api/mcp 调进来的 Agent)带了 W3C traceparent 就复用其 trace_id,
262
- # 这样两边的链路能对齐;非法/缺失则自己开一条(不信外部输入)。
263
243
  inbound = parse_traceparent(request.headers.get("traceparent"))
264
244
  tr = start_trace(f"{request.method} {path}", trace_id=inbound)
265
245
  try:
@@ -286,7 +266,6 @@ async def _metrics_mw(request, call_next):
286
266
  return resp
287
267
  finally:
288
268
  ms = (_t.perf_counter() - t0) * 1000.0
289
- # 只按已匹配的路由模板分组;未匹配(404)统一记 "unmatched",防攻击者刷不同路径撑爆 key
290
269
  route = request.scope.get("route")
291
270
  ep = getattr(route, "path", None) or "unmatched"
292
271
  try:
@@ -302,8 +281,6 @@ def install_middleware(app):
302
281
  顺序即执行顺序(Starlette 中间件后加先执行),必须与搬移前完全一致:
303
282
  _api_key_guard → _gateway_mw → _trace_mw → _metrics_mw。
304
283
  """
305
- # 【P3-loopback】启动审计日志:鉴权开启 + loopback bypass 默认开 → 打一行告知
306
- # 运维者。设了 FOXCLAW_API_KEY 但又不想本机免密,显式 FOXCLAW_LOOPBACK_BYPASS=0。
307
284
  if os.environ.get("FOXCLAW_API_KEY", "").strip() and _loopback_bypass_on():
308
285
  import logging
309
286
  logging.getLogger("foxclaw").info(