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
@@ -50,25 +50,22 @@ def _pid_alive(pid: int) -> bool:
50
50
  handle = kernel32.OpenProcess(
51
51
  PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
52
52
  if not handle:
53
- return False # 打不开句柄 = 进程不存在(或无权限)→ 保守判不存活
53
+ return False
54
54
  try:
55
55
  code = wintypes.DWORD()
56
56
  if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
57
57
  return False
58
- # STILL_ACTIVE=259 表示仍在运行。极少数进程恰以 259 退出会被误判存活,
59
- # 但这是"只读探测"的可接受代价——绝不误杀才是首要目标(psutil 同此取舍)。
60
58
  return code.value == STILL_ACTIVE
61
59
  finally:
62
60
  kernel32.CloseHandle(handle)
63
61
 
64
- # POSIX
65
62
  try:
66
63
  os.kill(pid, 0)
67
64
  return True
68
65
  except ProcessLookupError:
69
66
  return False
70
67
  except PermissionError:
71
- return True # 存在但无权限 → 视为存活
68
+ return True
72
69
  except Exception:
73
70
  return False
74
71
 
@@ -79,14 +76,13 @@ class TaskRecord:
79
76
  description: str
80
77
  pid: int
81
78
  started: float
82
- status: str = "in_progress" # in_progress / done / failed / interrupted
79
+ status: str = "in_progress"
83
80
  session_id: str = ""
84
81
  channel: str = ""
85
82
  finished: float = 0.0
86
- boot_id: str = "" # 哪次启动产生的(防 pid 复用误判)
87
- # 【P2-4】恢复策略字段
88
- retryable: bool = False # 调用方标记"可安全重试"(无副作用/幂等)
89
- start_count: int = 1 # 同一 task_id 被 start 的次数(重复 start 保留最后一次)
83
+ boot_id: str = ""
84
+ retryable: bool = False
85
+ start_count: int = 1
90
86
 
91
87
  def to_dict(self) -> dict:
92
88
  return asdict(self)
@@ -95,10 +91,10 @@ class TaskRecord:
95
91
  @dataclass
96
92
  class RecoveryPlan:
97
93
  """【P2-4】恢复计划:把中断任务分类,调用方(API 层/前端)据此决定动作。"""
98
- retryable: list = field(default_factory=list) # 可重试的 task_id
99
- notify_user: list = field(default_factory=list) # 需通知用户的(来自某个渠道)
100
- needs_manual: list = field(default_factory=list) # 需人工处理的(保守默认)
101
- details: list = field(default_factory=list) # [{task_id,description,channel,...,action}]
94
+ retryable: list = field(default_factory=list)
95
+ notify_user: list = field(default_factory=list)
96
+ needs_manual: list = field(default_factory=list)
97
+ details: list = field(default_factory=list)
102
98
 
103
99
  def to_dict(self) -> dict:
104
100
  return asdict(self)
@@ -107,20 +103,17 @@ class RecoveryPlan:
107
103
  class RestartRecovery:
108
104
  """任务生命周期日志 + 启动恢复扫描。"""
109
105
 
110
- def __init__(self, log_path: str, current_pid: int = None, boot_id: str = None,
106
+ def __init__(self, log_path: str, current_pid: int | None = None, boot_id: str | None = None,
111
107
  spine=None, fsync: bool = True):
112
108
  self.log_path = log_path
113
109
  self.current_pid = current_pid or os.getpid()
114
- # boot_id:每次启动唯一。任务记录带上它,扫描时"非本次启动的 in_progress"
115
- # 即属于上一次运行 —— 比单纯靠 pid 可靠(pid 会被 OS 复用)。
116
110
  import uuid
117
111
  self.boot_id = boot_id or uuid.uuid4().hex[:16]
118
- self._active: dict[str, TaskRecord] = {} # 当前进程内进行中的任务
119
- self._spine = spine # 【P2-4】审计脊柱(可空,None 静默)
120
- self._fsync = fsync # 【P2-4】fsync 可配(默认开=崩溃安全;高频场景可关)
112
+ self._active: dict[str, TaskRecord] = {}
113
+ self._spine = spine
114
+ self._fsync = fsync
121
115
  os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
122
116
 
123
- # ── 任务生命周期 ──────────────────────────────────────
124
117
  def task_started(self, task_id: str, description: str,
125
118
  session_id: str = "", channel: str = "",
126
119
  retryable: bool = False) -> TaskRecord:
@@ -142,7 +135,6 @@ class RestartRecovery:
142
135
  "status": status, "pid": self.current_pid,
143
136
  "finished": time.time()})
144
137
 
145
- # ── 步骤级 checkpoint(P1 断点可续)─────────────────────
146
138
  def record_step(self, task_id: str, step: dict) -> None:
147
139
  """记一条「已完成的步骤」。管线每完成一个 stage、工具轮每完成一个工具调用各落一条,
148
140
  崩溃/断连后 `steps_of()` 能回答「跑到哪一步」,也让续跑知道哪些步骤不必重做。
@@ -171,7 +163,7 @@ class RestartRecovery:
171
163
  try:
172
164
  ev = json.loads(line)
173
165
  except Exception:
174
- continue # 末行可能写一半(append 崩溃),跳过
166
+ continue
175
167
  if ev.get("event") == "step" and ev.get("task_id") == task_id:
176
168
  out.append({k: v for k, v in ev.items()
177
169
  if k not in ("event", "task_id")})
@@ -179,7 +171,6 @@ class RestartRecovery:
179
171
  return out
180
172
  return out[-limit:] if limit else out
181
173
 
182
- # ── 启动恢复扫描 ──────────────────────────────────────
183
174
  def scan_interrupted(self) -> list[TaskRecord]:
184
175
  """启动时调用:重建日志状态,找出被中断的任务。
185
176
 
@@ -194,9 +185,7 @@ class RestartRecovery:
194
185
  if rec.status != "in_progress":
195
186
  continue
196
187
  if rec.boot_id == self.boot_id:
197
- continue # 本次启动自己的任务,跳过
198
- # 非本次启动的 in_progress = 上次运行遗留 = 被中断
199
- # (pid 是否存活作为辅助信息记录,但不作为唯一判据)
188
+ continue
200
189
  rec.status = "interrupted"
201
190
  interrupted.append(rec)
202
191
  self._append({"event": "interrupted", "task_id": task_id,
@@ -220,7 +209,7 @@ class RestartRecovery:
220
209
  try:
221
210
  ev = json.loads(line)
222
211
  except Exception:
223
- continue # 跳过损坏行(append 模式下末行可能写一半)
212
+ continue
224
213
  self._apply_event(records, ev)
225
214
  except Exception:
226
215
  pass
@@ -233,7 +222,6 @@ class RestartRecovery:
233
222
  return
234
223
  kind = ev.get("event")
235
224
  if kind == "start":
236
- # 【P2-4】同一 task_id 多次 start:保留最后一次,但累计 start_count
237
225
  existing = records.get(tid)
238
226
  start_count = (existing.start_count + 1) if existing else 1
239
227
  records[tid] = TaskRecord(
@@ -249,7 +237,7 @@ class RestartRecovery:
249
237
  if kind == "finish":
250
238
  records[tid].finished = ev.get("finished", 0)
251
239
 
252
- def plan_recovery(self, interrupted: list = None) -> RecoveryPlan:
240
+ def plan_recovery(self, interrupted: list | None = None) -> RecoveryPlan:
253
241
  """【P2-4】给中断任务分类,返回恢复计划(不改 scan_interrupted 的返回,兼容)。
254
242
 
255
243
  分类规则:
@@ -304,9 +292,9 @@ class RestartRecovery:
304
292
  f.write(json.dumps(entry, ensure_ascii=False) + "\n")
305
293
  f.flush()
306
294
  if self._fsync:
307
- os.fsync(f.fileno()) # 落盘,防崩溃丢最后一条
295
+ os.fsync(f.fileno())
308
296
  except Exception:
309
- pass # 日志失败不影响主流程
297
+ pass
310
298
 
311
299
  def compact_log(self, keep_recent_days: float = 7):
312
300
  """压实日志:丢弃已完成且超过 N 天的记录,防文件无限膨胀。
@@ -317,7 +305,6 @@ class RestartRecovery:
317
305
  cutoff = time.time() - keep_recent_days * 86400
318
306
  keep = []
319
307
  for rec in records.values():
320
- # 保留:仍在进行的,或最近有活动(开始或完成)的
321
308
  last_activity = max(rec.started, rec.finished or 0)
322
309
  if rec.status == "in_progress" or last_activity >= cutoff:
323
310
  keep.append({"event": "start", **rec.to_dict()})
@@ -331,7 +318,7 @@ class RestartRecovery:
331
318
  with open(tmp, "w", encoding="utf-8") as f:
332
319
  for e in keep:
333
320
  f.write(json.dumps(e, ensure_ascii=False) + "\n")
334
- for e in steps: # 保留任务的 step 事件随记录一起搬走
321
+ for e in steps:
335
322
  f.write(json.dumps(e, ensure_ascii=False) + "\n")
336
323
  os.replace(tmp, self.log_path)
337
324
  except Exception:
@@ -9,45 +9,46 @@
9
9
  """
10
10
  from __future__ import annotations
11
11
  from dataclasses import dataclass
12
+ from typing import Optional
12
13
 
13
14
 
14
15
  @dataclass
15
16
  class TaskIntent:
16
17
  """任务意图:粗粒度类别 + 三个亲和度提示。"""
17
- category: str # reasoning / memory / workflow / tool
18
+ category: str
18
19
  confidence: float
19
- memory_affinity: float # 记忆依赖度
20
- reasoning_depth: float # 推理深度提示
20
+ memory_affinity: float
21
+ reasoning_depth: float
21
22
 
22
23
 
23
24
  @dataclass
24
25
  class TaskFeatures:
25
- complexity: float # 复杂度
26
- memory_dependency: float # 记忆依赖
27
- tool_cost: float # 工具成本
28
- reasoning_depth: float # 推理深度
29
- urgency: float # 紧急度
30
- expected_runtime: float # 预期耗时
26
+ complexity: float
27
+ memory_dependency: float
28
+ tool_cost: float
29
+ reasoning_depth: float
30
+ urgency: float
31
+ expected_runtime: float
31
32
 
32
33
 
33
34
  @dataclass
34
35
  class SystemContext:
35
36
  """系统负载上下文。主管据此调整优先级、决定是否抢占。"""
36
- active_agents: int = 0 # 当前活跃 Agent 数
37
- queue_depth: int = 0 # 待处理任务队列深度
38
- system_load: float = 0.1 # 系统负载 0-1
37
+ active_agents: int = 0
38
+ queue_depth: int = 0
39
+ system_load: float = 0.1
39
40
 
40
41
 
41
42
  @dataclass
42
43
  class ScheduleDecision:
43
- priority: float # 最终优先级 0-1
44
- route_hint: str # 路由倾向:agent / tool / workflow
45
- execution_mode: str # preemptive(抢占) / standard(标准)
46
- preemptible: bool # 是否可触发抢占
47
- expected_budget: float # 预期预算(耗时×优先级修正)
48
- intent: TaskIntent = None
49
- features: TaskFeatures = None
50
- preemption_reason: str = None
44
+ priority: float
45
+ route_hint: str
46
+ execution_mode: str
47
+ preemptible: bool
48
+ expected_budget: float
49
+ intent: Optional[TaskIntent] = None
50
+ features: Optional[TaskFeatures] = None
51
+ preemption_reason: Optional[str] = None
51
52
  reason: str = ""
52
53
 
53
54
  def to_dict(self) -> dict:
@@ -63,7 +64,6 @@ class ScheduleDecision:
63
64
  class CognitiveScheduler:
64
65
  """意图感知的认知调度器。"""
65
66
 
66
- # 意图识别关键词(粗分类)
67
67
  _REASONING = ["规划", "计划", "分析", "设计", "推理", "为什么", "怎么办", "策略", "方案"]
68
68
  _MEMORY = ["记得", "记忆", "之前", "上次", "回忆", "查一下我", "我说过"]
69
69
  _WORKFLOW = ["然后", "接着", "之后", "依次", "批量", "流程", "再然后", "下一步"]
@@ -86,7 +86,6 @@ class CognitiveScheduler:
86
86
  complexity, tool_cost, urgency, runtime = 0.7, 0.6, 0.65, 0.8
87
87
  elif intent.category == "memory":
88
88
  complexity, tool_cost, urgency, runtime = 0.5, 0.2, 0.55, 0.6
89
- # 紧急词加成
90
89
  if any(w in task for w in ["紧急", "立刻", "马上", "尽快", "急"]):
91
90
  urgency = min(1.0, urgency + 0.3)
92
91
  return TaskFeatures(complexity, intent.memory_affinity, tool_cost,
@@ -117,16 +116,14 @@ class CognitiveScheduler:
117
116
  return "tool"
118
117
  return "agent" if priority > 0.35 else "tool"
119
118
 
120
- def preemption_reason(self, priority: float, ctx: SystemContext) -> str:
121
- # 阈值匹配实际优先级范围(本调度器优先级多在 0.4 以内,
122
- # 原 0.55/0.7 几乎不可达,会让抢占形同虚设)
119
+ def preemption_reason(self, priority: float, ctx: SystemContext) -> str | None:
123
120
  if priority > 0.35 and ctx.active_agents >= 3:
124
121
  return "高优先级 + Agent 压力大"
125
122
  if priority > 0.4 and ctx.queue_depth >= 2:
126
123
  return "高优先级 + 队列积压"
127
124
  return None
128
125
 
129
- def schedule(self, task: str, ctx: SystemContext = None) -> ScheduleDecision:
126
+ def schedule(self, task: str, ctx: SystemContext | None = None) -> ScheduleDecision:
130
127
  """主入口:给一个任务做完整调度决策。"""
131
128
  ctx = ctx or SystemContext()
132
129
  intent = self.parse_intent(task)
@@ -22,8 +22,6 @@ import base64
22
22
 
23
23
  SERVICE = "foxclaw.llm"
24
24
 
25
- # 后端探测结果缓存(含失败):每个请求都 import + 探测一次没必要。
26
- # 失败也缓存是有意的 —— 没有可用后端时不该反复重试。
27
25
  _kr_cache = None
28
26
  _kr_probed = False
29
27
 
@@ -37,7 +35,6 @@ def _keyring():
37
35
  try:
38
36
  import keyring
39
37
  from keyring.backends.fail import Keyring as _FailBackend
40
- # 默认后端是 Fail 时说明系统上没有可用凭据库(典型:无桌面会话的 Linux)
41
38
  if isinstance(keyring.get_keyring(), _FailBackend):
42
39
  _kr_cache = None
43
40
  else:
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
  import json
17
17
  import re
18
+ import builtins
18
19
  import threading
19
20
  import time
20
21
  import uuid
@@ -42,9 +43,7 @@ class SessionStore:
42
43
  self.base.mkdir(parents=True, exist_ok=True)
43
44
  self._lock = threading.RLock()
44
45
 
45
- # ── 内部 ──────────────────────────────────────────────
46
46
  def _path(self, sid: str) -> Path:
47
- # 只允许我们自己生成的 id 形态,杜绝 ../ 之类路径穿越
48
47
  if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", str(sid or "")):
49
48
  raise ValueError("非法会话 id")
50
49
  return self.base / f"{sid}.json"
@@ -64,9 +63,8 @@ class SessionStore:
64
63
  tmp = p.with_suffix(".tmp")
65
64
  with open(tmp, "w", encoding="utf-8") as f:
66
65
  json.dump(data, f, ensure_ascii=False, indent=1)
67
- tmp.replace(p) # 原子替换,防写一半崩坏
66
+ tmp.replace(p)
68
67
 
69
- # ── 公开 API ──────────────────────────────────────────
70
68
  def create(self, title: str = "", agent_id: str = "supervisor") -> dict:
71
69
  sid = uuid.uuid4().hex[:12]
72
70
  data = {"id": sid, "title": (title or "新会话")[:60], "agent_id": agent_id,
@@ -105,7 +103,8 @@ class SessionStore:
105
103
  with self._lock:
106
104
  return self._read(sid)
107
105
 
108
- def append(self, sid: str, role: str, content: str, attachments: list = None) -> bool:
106
+ def append(self, sid: str, role: str, content: str,
107
+ attachments: builtins.list | None = None) -> bool:
109
108
  """追加一条消息;会话不存在则忽略。首条用户消息会顺带定标题。
110
109
  attachments(可选)= 聊天附件元数据 [{name,size,mime,url}];只落盘轻字段(不存 text,
111
110
  避免会话文件随附件正文膨胀),供「会话历史」回看时渲染附件卡片。"""
@@ -128,7 +127,6 @@ class SessionStore:
128
127
  if atts:
129
128
  msg["attachments"] = atts
130
129
  msgs.append(msg)
131
- # 标题仍是默认值时,用首条用户消息提炼
132
130
  if role == "user" and (not d.get("title") or d["title"] == "新会话"):
133
131
  d["title"] = derive_title(content)
134
132
  d["updated_at"] = _now()
@@ -45,8 +45,6 @@ from functools import lru_cache
45
45
  from pathlib import Path
46
46
  from typing import Any, Iterable, Mapping, Sequence
47
47
 
48
- # provider → 需要的环境变量(与 core/model_router.PROVIDER_ENV 保持一致;
49
- # 这里重复一份是为了让 settings 不依赖 model_router —— 那会拖入 LLM SDK 的导入)。
50
48
  PROVIDER_KEY_ENV: dict[str, str] = {
51
49
  "anthropic": "ANTHROPIC_API_KEY",
52
50
  "openai": "OPENAI_API_KEY",
@@ -58,8 +56,6 @@ PROVIDER_KEY_ENV: dict[str, str] = {
58
56
  "generic": "LLM_API_KEY",
59
57
  }
60
58
 
61
- # 只检查这些前缀下的"未知键":它们属于本项目或本项目专用的厂商约定,
62
- # 出现没人读的名字几乎必然是拼错。像 `PATH` / `HOME` 这类系统变量不在此列。
63
59
  OWNED_PREFIXES = (
64
60
  "FOXCLAW_", "BAIDU_", "AMAP_", "LLM_", "GENERIC_LLM_",
65
61
  "MINIMAX_", "LOCAL_LLM_", "HKC_", "HMR_",
@@ -75,10 +71,10 @@ class Opt:
75
71
  """一个环境变量的声明。kind 只描述**解析方式**,不代表业务语义。"""
76
72
 
77
73
  name: str
78
- kind: str = "str" # str | int | float | csv | choice
74
+ kind: str = "str"
79
75
  default: Any = ""
80
76
  group: str = "其他"
81
- hint: str = "" # 期望格式 / 示例(拼进出错信息)
77
+ hint: str = ""
82
78
  choices: Sequence[str] = ()
83
79
  secret: bool = False
84
80
  lo: float | None = None
@@ -86,10 +82,6 @@ class Opt:
86
82
 
87
83
 
88
84
  SPECS: tuple[Opt, ...] = (
89
- # lo/hi 只给**调用点自己不兜底**的项。像 TRACE_RETAIN 已有 max(100, ...)、
90
- # BAIDU_SEARCH_TIMEOUT 已有 max(5, ...):在那里再报一次范围错,等于把"clamp"
91
- # 变成"整条配置作废退默认",反而更糟。
92
- # ── 模型 ────────────────────────────────────────────────────────────────
93
85
  Opt("LLM_PROVIDER", "choice", "", "模型",
94
86
  "anthropic / openai / deepseek / google / qwen / local / minimax / generic",
95
87
  tuple(PROVIDER_KEY_ENV)),
@@ -109,9 +101,10 @@ SPECS: tuple[Opt, ...] = (
109
101
  Opt("LLM_API_KEY", "str", "", "模型", "generic(通用 openai 兼容)的 key", secret=True),
110
102
  Opt("LLM_BASE_URL", "str", "", "模型", "generic 的 base_url,如 https://host/v1"),
111
103
  Opt("LOCAL_LLM_BASE", "str", "", "模型", "本地推理服务的 base_url"),
112
- # ── 服务 ────────────────────────────────────────────────────────────────
113
104
  Opt("FOXCLAW_PORT", "int", 8000, "服务", "HTTP 监听端口", lo=1, hi=65535),
114
105
  Opt("FOXCLAW_DATA_DIR", "str", "", "服务", "数据目录(默认 <仓库>/foxclaw_data)"),
106
+ Opt("FOXCLAW_CLAWHUB_REGISTRY", "str", "", "服务",
107
+ "ClawHub 地址(默认 https://clawhub.ai;内网镜像或自建 registry 填这里)"),
115
108
  Opt("FOXCLAW_IMPORT_ROOTS", "csv", "", "服务",
116
109
  "额外允许的导入目录(逗号分隔);不配则只有数据目录内的文件可被导入"),
117
110
  Opt("FOXCLAW_API_KEY", "str", "", "服务",
@@ -127,7 +120,6 @@ SPECS: tuple[Opt, ...] = (
127
120
  Opt("FOXCLAW_STREAM_RATE", "int", 60, "服务", "流式端点每分钟上限", lo=0),
128
121
  Opt("FOXCLAW_STREAM_CONCURRENCY", "int", 4, "服务", "流式并发上限", lo=1),
129
122
  Opt("FOXCLAW_SHADOW_ROUNDS", "int", 20, "服务", "影子推演轮数", lo=1),
130
- # ── 工具 / 追踪 ─────────────────────────────────────────────────────────
131
123
  Opt("FOXCLAW_TOOL_TIMEOUT", "float", 45.0, "工具",
132
124
  "单个工具调用硬超时(秒);可小于 1(测试用),调用点会兜 >0"),
133
125
  Opt("FOXCLAW_TRACE_SLOW_MS", "float", 1000.0, "追踪", "超过多少毫秒算慢请求", lo=0),
@@ -135,12 +127,10 @@ SPECS: tuple[Opt, ...] = (
135
127
  "非慢请求的采样率 0~1;超出范围由调用点 clamp,不在这里报错"),
136
128
  Opt("FOXCLAW_TRACE_RETAIN", "int", 5000, "追踪",
137
129
  "内存里保留的 trace 条数;调用点已有 max(100, ...) 兜底"),
138
- # ── 搜索 ────────────────────────────────────────────────────────────────
139
130
  Opt("BAIDU_SEARCH_KEY", "str", "", "搜索", "百度 AI 搜索的 key", secret=True),
140
131
  Opt("BAIDU_SEARCH_MODEL", "str", "", "搜索", "百度搜索用的模型名"),
141
132
  Opt("BAIDU_SEARCH_BASE", "str", "", "搜索", "百度搜索的 base_url"),
142
133
  Opt("BAIDU_SEARCH_TIMEOUT", "int", 45, "搜索", "秒;调用点已有 max(5, ...) 兜底"),
143
- # ── 渠道 ────────────────────────────────────────────────────────────────
144
134
  Opt("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "csv", "", "渠道",
145
135
  "逗号分隔的可信发送者:<id> / <channel>:<id> / <channel>:*"),
146
136
  Opt("FOXCLAW_CHANNEL_TRUST_ALL", "str", "", "渠道",
@@ -148,27 +138,24 @@ SPECS: tuple[Opt, ...] = (
148
138
  Opt("FOXCLAW_CHANNEL_SIGNING_SECRET", "str", "", "渠道",
149
139
  "出站 HMAC 密钥;留空 = 不签", secret=True),
150
140
  Opt("FOXCLAW_CHANNEL_SIGNING_KEY_ID", "str", "", "渠道", "密钥轮换标识"),
151
- # ── 浏览器 ──────────────────────────────────────────────────────────────
152
141
  Opt("FOXCLAW_BROWSER_ALLOW_PRIVATE", "str", "", "浏览器",
153
142
  "1/true/yes 才允许访问内网地址"),
154
143
  )
155
144
 
156
145
  SPEC_BY_NAME: dict[str, Opt] = {o.name: o for o in SPECS}
157
146
 
158
- # 会在启动/运行早期被读、且类型写错就会崩的少数几个 —— 供 serve 预检快速拦截。
159
147
  FATAL_CODES = ("type", "choice", "range")
160
148
 
161
149
 
162
150
  @dataclass
163
151
  class Issue:
164
- level: str # "error" | "warn"
165
- code: str # type / choice / range / provider_key / unknown_key ...
152
+ level: str
153
+ code: str
166
154
  name: str
167
155
  message: str
168
- hint: str = "" # 修复建议(可为空)
156
+ hint: str = ""
169
157
 
170
158
  def __str__(self) -> str:
171
- # 只用 ASCII 记号:Windows 控制台默认 GBK,✓/✗/⚠ 会抛 UnicodeEncodeError
172
159
  mark = "ERROR" if self.level == "error" else " WARN"
173
160
  head = f"[{mark}] {self.name}: {self.message}" if self.name else f"[{mark}] {self.message}"
174
161
  return f"{head}\n → {self.hint}" if self.hint else head
@@ -178,7 +165,7 @@ def _caller() -> str:
178
165
  """当前 `get_*` 的调用点(文件:行)。只在出错路径上求值,不进热路径。"""
179
166
  try:
180
167
  f = inspect.currentframe()
181
- for _ in range(3): # 跳过 get_* → _parse → _caller 自身
168
+ for _ in range(3):
182
169
  if f is None:
183
170
  return ""
184
171
  f = f.f_back
@@ -210,16 +197,16 @@ def _parse(opt: Opt, raw: str, where: str = "") -> Any:
210
197
  return value
211
198
 
212
199
  if opt.kind == "choice":
213
- value = text.lower()
214
- if value not in opt.choices:
200
+ choice = text.lower()
201
+ if choice not in opt.choices:
215
202
  raise ConfigError(
216
203
  f'值 "{raw}" 不在允许范围内。可选:{" / ".join(opt.choices)}{suffix}')
217
- return value
204
+ return choice
218
205
 
219
206
  if opt.kind == "csv":
220
207
  return [s.strip() for s in text.split(",") if s.strip()]
221
208
 
222
- return raw # str:保留原样(尾部空格可能有意义,如口令)
209
+ return raw
223
210
 
224
211
 
225
212
  def _opt(name: str) -> Opt:
@@ -251,7 +238,7 @@ _warned: set[str] = set()
251
238
  _log = logging.getLogger("foxclaw.config")
252
239
 
253
240
 
254
- def get_safe(name: str, default: Any = None) -> Any:
241
+ def get_safe(name: str, default: Any | None = None) -> Any:
255
242
  """可降级的读取:值非法时**退默认值并要求日志警告一次**,不抛异常。
256
243
 
257
244
  给"配置错不该拖垮主流程"的地方用(trace 采样、工具超时、搜索超时):这些点
@@ -262,12 +249,12 @@ def get_safe(name: str, default: Any = None) -> Any:
262
249
  fallback = opt.default if default is None else default
263
250
  raw = os.environ.get(name)
264
251
  if raw is None or not raw.strip():
265
- return fallback # 未设置/空串:用调用方给的兜底
252
+ return fallback
266
253
  try:
267
254
  return _parse(opt, raw, _caller())
268
255
  except ConfigError as e:
269
256
  if name not in _warned:
270
- _warned.add(name) # 只喊一次:tracing 是每请求调用
257
+ _warned.add(name)
271
258
  _log.warning("[config] %s", e)
272
259
  return fallback
273
260
 
@@ -297,8 +284,6 @@ def known_keys() -> frozenset[str]:
297
284
  只在 `check()` / `show()` 这类诊断路径调用,不进请求路径;结果缓存在本进程内。
298
285
  """
299
286
  pkg = Path(__file__).resolve().parent.parent
300
- # vendor/(HKC/HMR)在仓库根,不在包内:它们自己读 HMR_*/HKC_* 变量,不一起扫就会把
301
- # 正常配置报成"拼错的键"。wheel 里没有 vendor/ → 扫描自然为空,不影响安装后的行为。
302
287
  roots = [pkg] + ([pkg.parent / "vendor"] if (pkg.parent / "vendor").is_dir() else [])
303
288
  found: set[str] = set(SPEC_BY_NAME)
304
289
  files: list[Path] = []
@@ -308,7 +293,7 @@ def known_keys() -> frozenset[str]:
308
293
  for path in files:
309
294
  try:
310
295
  tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
311
- except SyntaxError: # 语法坏掉的文件留着给 CI 报,这里跳过
296
+ except SyntaxError:
312
297
  continue
313
298
  for node in ast.walk(tree):
314
299
  if isinstance(node, ast.Call):
@@ -401,7 +386,6 @@ def check(env: Mapping[str, str] | None = None) -> list[Issue]:
401
386
  except ConfigError as e:
402
387
  code = "choice" if opt.kind == "choice" else (
403
388
  "range" if ("下限" in str(e) or "上限" in str(e)) else "type")
404
- # 密钥不回显原值,免得把半截口令打进日志
405
389
  msg = str(e)
406
390
  if is_secret(opt.name):
407
391
  msg = f"值 {mask(raw)} 不合法(原始值已脱敏)"
@@ -42,21 +42,12 @@ from typing import Iterable, Optional
42
42
  MODE_ENV = "FOXCLAW_SHARED_WRITE_MODE"
43
43
  VALID_MODES = ("audit", "warn", "enforce")
44
44
 
45
- # 与写入方无关、任何 Agent 都可写的 facet(设计上就该跨 Agent 共享的东西):
46
- # preference —— 用户偏好:base.py 明确以 shared=True 写入,意在跨会话跨 Agent 生效
47
- # truth_pipeline —— 真值管线沉淀(truth_pipeline.py),写入方是 agent_id="system"
48
45
  GLOBAL_ALLOWED_FACETS = frozenset({"preference", "truth_pipeline"})
49
46
 
50
- # 各 Agent 的「职责域」允许 facet。facet 取自一次写入的 {type:<记忆类型>} ∪ tags,
51
- # 支持 fnmatch 通配(如 finance.* 可匹配 finance.budget)。
52
- #
53
- # 注意:这是**起始假设**,不是已验证的结论 —— 默认 audit 模式的目的正是用真实流量验证它。
54
47
  DEFAULT_SHARED_WRITE_ALLOWED = {
55
- "supervisor": {"*"}, # 主管:分发/协调记录,语义上允许任意
56
- "system": {"*"}, # 系统(真值管线、后台任务)
48
+ "supervisor": {"*"},
49
+ "system": {"*"},
57
50
  "finance": {"finance.*"},
58
- # 行政并入办公后,办公学习 Agent 同时拥有学习与行程两个写域;
59
- # 助手已下放为技能(不再有独立 agent_id),不再单列 ACL 条目。
60
51
  "office": {"learning.*", "itinerary.*", "schedule.*"},
61
52
  }
62
53
 
@@ -90,8 +81,8 @@ class SharedWritePolicy:
90
81
  无状态、无锁、纯函数式判定 —— 并发安全直接成立,也便于单独测试。
91
82
  """
92
83
 
93
- def __init__(self, allowed: dict = None, mode: str = None,
94
- global_facets: Iterable[str] = None):
84
+ def __init__(self, allowed: dict | None = None, mode: str | None = None,
85
+ global_facets: Iterable[str] | None = None):
95
86
  self.allowed = {k: set(v) for k, v in
96
87
  (allowed or DEFAULT_SHARED_WRITE_ALLOWED).items()}
97
88
  self.global_facets = set(global_facets if global_facets is not None
@@ -99,13 +90,11 @@ class SharedWritePolicy:
99
90
  self.mode = self._resolve_mode(mode)
100
91
 
101
92
  @staticmethod
102
- def _resolve_mode(mode: str = None) -> str:
93
+ def _resolve_mode(mode: str | None = None) -> str:
103
94
  """显式入参 > 环境变量 > 默认 audit。非法值一律回落到 audit(保守:不拦截)。"""
104
95
  m = (mode or os.environ.get(MODE_ENV, "") or "audit").strip().lower()
105
96
  return m if m in VALID_MODES else "audit"
106
97
 
107
- # 纯标记类 tag:描述"这条记忆的归属/去向",不描述"内容是什么"。
108
- # 让它们参与内容域匹配只会稀释策略表 —— 尤其 "shared" 在本闸门语境下恒为真,零信息量。
109
98
  _MARKER_TAGS = ("agent:", "shared")
110
99
 
111
100
  @staticmethod
@@ -126,9 +115,9 @@ class SharedWritePolicy:
126
115
  out.append(t)
127
116
  return tuple(dict.fromkeys(out))
128
117
 
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)
118
+ def evaluate(self, agent_id: str | None = None, memory_type: str = "concept",
119
+ tags: Iterable[str] | None = None) -> Decision:
120
+ facets = self.facets_of(memory_type, tags or ())
132
121
  who = agent_id or "unknown"
133
122
 
134
123
  if any(fnmatch.fnmatchcase(f, p) for f in facets for p in self.global_facets):
@@ -142,9 +131,9 @@ class SharedWritePolicy:
142
131
  return Decision(False, f"{who} 的职责域不含 {list(facets)}", self.mode, facets)
143
132
 
144
133
 
145
- def guard_shared_write(backend, agent_id: str, content: str,
134
+ def guard_shared_write(backend, agent_id: str | None, content: str,
146
135
  memory_type: str = "concept",
147
- tags: Iterable[str] = None) -> Optional[Decision]:
136
+ tags: Iterable[str] | None = None) -> Optional[Decision]:
148
137
  """共享层写入的统一闸门 —— 必须在**真正落库之前**调用。
149
138
 
150
139
  行为分三档:
@@ -170,7 +159,6 @@ def guard_shared_write(backend, agent_id: str, content: str,
170
159
  "allowed": decision.allowed,
171
160
  "reason": decision.reason,
172
161
  "mode": decision.mode,
173
- # 只留摘要哈希 + 短预览,不把共享层正文原样灌进事件流
174
162
  "content_sha": hashlib.sha256(
175
163
  str(content or "").encode("utf-8")).hexdigest()[:16],
176
164
  "content_preview": str(content or "")[:60],
@@ -187,7 +175,7 @@ def guard_shared_write(backend, agent_id: str, content: str,
187
175
  return decision
188
176
 
189
177
 
190
- def attach(backend, policy: SharedWritePolicy = None, spine=None) -> SharedWritePolicy:
178
+ def attach(backend, policy: SharedWritePolicy | None = None, spine=None) -> SharedWritePolicy:
191
179
  """把策略挂到记忆后端(HMRAdapter / MockHMR 皆可)。返回生效的策略实例。
192
180
 
193
181
  用挂载而非构造函数注入,是因为装配顺序:memory_adapter 在 event_spine **之前**创建,