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
@@ -25,7 +25,6 @@ import re
25
25
  from dataclasses import dataclass, field
26
26
 
27
27
 
28
- # 粗略 token 估算:中文约 1 字符 ≈ 1 token,英文约 4 字符 ≈ 1 token。
29
28
  def estimate_tokens(text: str) -> int:
30
29
  if not text:
31
30
  return 0
@@ -38,8 +37,6 @@ def estimate_messages_tokens(messages: list) -> int:
38
37
  return sum(estimate_tokens(str(m.get("content", ""))) for m in messages)
39
38
 
40
39
 
41
- # 关键标识符:不能在摘要中丢失的(ID、长数字、订单号、哈希、金额等)
42
- # 不用外层 \b:¥/逗号不是单词字符,会导致金额匹配失败。
43
40
  _IDENTIFIER_PAT = re.compile(
44
41
  r"[a-zA-Z]{2,}_[a-zA-Z0-9]{4,}|[a-f0-9]{8,}|[0-9]{6,}|¥[\d,]+(?:\.\d+)?")
45
42
 
@@ -56,14 +53,14 @@ def extract_identifiers(messages: list) -> list[str]:
56
53
 
57
54
  @dataclass
58
55
  class CompactionResult:
59
- messages: list = field(default_factory=list) # 压缩后的消息列表
60
- method: str = "none" # llm_summary/partial_summary/truncate/none
56
+ messages: list = field(default_factory=list)
57
+ method: str = "none"
61
58
  original_count: int = 0
62
59
  compacted_count: int = 0
63
60
  original_tokens: int = 0
64
61
  compacted_tokens: int = 0
65
62
  preserved_identifiers: list = field(default_factory=list)
66
- note: str = "" # 说明(如压缩跳过原因)
63
+ note: str = ""
67
64
 
68
65
  def to_dict(self) -> dict:
69
66
  return {
@@ -101,8 +98,6 @@ def group_tool_calls(messages: list) -> list:
101
98
  m = messages[i]
102
99
  buf.append(m)
103
100
  if m.get("role") == "assistant" and (m.get("tool_calls") or []):
104
- # 本 assistant 声明了工具调用 → 把紧随其后、id 匹配的 tool 结果
105
- # 全部并入同一组,确保「声明与结果同进同出」。
106
101
  ids = {tc.get("id") for tc in (m.get("tool_calls") or [])}
107
102
  j = i + 1
108
103
  while j < n:
@@ -125,10 +120,10 @@ class Compactor:
125
120
 
126
121
  def __init__(self, router=None, max_tokens: int = 6000,
127
122
  keep_recent: int = 4, oversized_token: int = 2000):
128
- self.router = router # ModelRouter,用于 LLM 摘要(可空→走截断兜底)
129
- self.max_tokens = max_tokens # 超过此 token 触发压缩
130
- self.keep_recent = keep_recent # 最近保留几条原样
131
- self.oversized_token = oversized_token # 单条超此值视为"超大消息"
123
+ self.router = router
124
+ self.max_tokens = max_tokens
125
+ self.keep_recent = keep_recent
126
+ self.oversized_token = oversized_token
132
127
 
133
128
  def needs_compaction(self, messages: list) -> bool:
134
129
  return estimate_messages_tokens(messages) > self.max_tokens
@@ -146,13 +141,9 @@ class Compactor:
146
141
  res.compacted_tokens = res.original_tokens
147
142
  return res
148
143
 
149
- # 分区:系统消息 / 待压缩的早期消息 / 保留的近期消息
150
144
  system_msgs = [m for m in messages if m.get("role") == "system"]
151
145
  non_system = [m for m in messages if m.get("role") != "system"]
152
146
  keep = max(0, self.keep_recent)
153
- # 按**原子组**保留(而非按条数),保证 assistant.tool_calls 与其 tool 结果
154
- # 同进同出。按条数切会在「一轮多工具」时把 tool 结果与其声明切散 →
155
- # provider 报 400。详见 group_tool_calls docstring。
156
147
  _groups = group_tool_calls(non_system)
157
148
  if keep and _groups:
158
149
  recent = [m for g in _groups[-keep:] for m in g]
@@ -161,8 +152,6 @@ class Compactor:
161
152
  recent, to_compress = [], non_system
162
153
  else:
163
154
  recent, to_compress = [], non_system
164
- # 防御:若仍不配对(极端边界/上游已产生的畸形序列),宁可放弃压缩,
165
- # 也不产出非法消息序列 —— 压缩不生效只是浪费 token,发出去 400 是整轮失败。
166
155
  _declared = {tc.get("id") for m in (recent + to_compress)
167
156
  for tc in (m.get("tool_calls") or [])}
168
157
  _orphans = [m for m in (recent + to_compress)
@@ -185,15 +174,12 @@ class Compactor:
185
174
 
186
175
  res.preserved_identifiers = extract_identifiers(to_compress)
187
176
 
188
- # —— 渐进降级 ——
189
177
  summary = None
190
178
  method = "truncate"
191
- # 1. LLM 完整摘要
192
179
  if self.router is not None:
193
180
  summary = self._try_summarize(to_compress, res.preserved_identifiers)
194
181
  if summary:
195
182
  method = "llm_summary"
196
- # 2. 部分摘要:排除超大消息后再试
197
183
  if summary is None and self.router is not None:
198
184
  trimmed = [m for m in to_compress
199
185
  if estimate_tokens(str(m.get("content", ""))) <= self.oversized_token]
@@ -201,7 +187,6 @@ class Compactor:
201
187
  summary = self._try_summarize(trimmed, res.preserved_identifiers)
202
188
  if summary:
203
189
  method = "partial_summary"
204
- # 3. 截断兜底
205
190
  if summary is None:
206
191
  summary = self._truncate_fallback(to_compress, res.preserved_identifiers)
207
192
  method = "truncate"
@@ -211,14 +196,11 @@ class Compactor:
211
196
  compacted = system_msgs + [summary_msg] + recent
212
197
  compacted_tokens = estimate_messages_tokens(compacted)
213
198
 
214
- # 校验:压缩须真的显著变小(至少省 10%),否则放弃——避免系统消息过大时
215
- # 压缩无效还白加摘要导致反而更大。
216
199
  if compacted_tokens >= res.original_tokens * 0.9:
217
200
  res.messages = list(messages)
218
201
  res.method = "skipped_ineffective"
219
202
  res.compacted_count = len(messages)
220
203
  res.compacted_tokens = res.original_tokens
221
- # 提示:压缩无效通常因系统消息/近期消息本身已占满预算
222
204
  sys_tokens = estimate_messages_tokens(system_msgs)
223
205
  res.note = (f"压缩跳过:有效压缩空间不足(系统消息占 {sys_tokens} token)。"
224
206
  f"建议精简系统提示或调大上下文预算。")
@@ -242,7 +224,7 @@ class Compactor:
242
224
  return list(messages), None
243
225
  res = self.compact(messages)
244
226
  if res.method == "none" or res.method.startswith("skipped"):
245
- return list(messages), res # 压缩没生效 → 原样,结果仅用于观测
227
+ return list(messages), res
246
228
  return res.messages, res
247
229
  except Exception:
248
230
  return list(messages), None
@@ -22,7 +22,7 @@ class LayeredConfig:
22
22
  """三层配置合并原语。持有各层 dict 的引用 —— 若某层的集合值是可变对象(如 SkillPolicy 的
23
23
  user deny set),原地增删会被 merged_set/policy_locked 实时反映,无需重建。"""
24
24
 
25
- def __init__(self, policy: dict = None, user: dict = None, runtime: dict = None):
25
+ def __init__(self, policy: dict | None = None, user: dict | None = None, runtime: dict | None = None):
26
26
  self.policy = dict(policy or {})
27
27
  self.user = dict(user or {})
28
28
  self.runtime = dict(runtime or {})
@@ -61,5 +61,5 @@ def load_policy_layer(data_dir=None) -> dict:
61
61
  d = json.loads(p.read_text(encoding="utf-8"))
62
62
  deny |= set(d.get("deny") or [])
63
63
  except Exception:
64
- pass # policy.json 损坏不应拖垮启动;宁可少锁不错锁,由运维排查
64
+ pass
65
65
  return {"deny": sorted(deny)}
@@ -25,10 +25,9 @@ import time
25
25
  from dataclasses import dataclass, field
26
26
  from typing import Optional, Tuple
27
27
 
28
- #: 视为「关键操作」的副作用级别(发送/支付);destructive 无条件算关键
29
28
  CRITICAL_SIDE_EFFECTS = ("send", "pay")
30
29
 
31
- DEFAULT_TTL = 300.0 # 秒;确认码有效期
30
+ DEFAULT_TTL = 300.0
32
31
 
33
32
 
34
33
  def action_fingerprint(action: str, args: dict) -> str:
@@ -78,14 +77,13 @@ class ConfirmationBroker:
78
77
  self._items: dict[str, PendingConfirm] = {}
79
78
  self._lock = threading.Lock()
80
79
 
81
- # ── 签发 ────────────────────────────────────────────
82
80
  def issue(self, session: str, action: str, args: dict,
83
- ttl: float = None) -> dict:
81
+ ttl: float | None = None) -> dict:
84
82
  """签发一个绑定到 (会话, 动作, 参数) 的一次性确认码。"""
85
83
  now = self._clock()
86
84
  ttl = self.ttl if ttl is None else float(ttl)
87
85
  item = PendingConfirm(
88
- nonce=secrets.token_hex(8), # 16 hex 字符,人类也能念出来
86
+ nonce=secrets.token_hex(8),
89
87
  action=action or "",
90
88
  fingerprint=action_fingerprint(action or "", args or {}),
91
89
  session=session or "",
@@ -99,9 +97,8 @@ class ConfirmationBroker:
99
97
  return {"nonce": item.nonce, "action": item.action,
100
98
  "expires_at": item.expires, "ttl": ttl}
101
99
 
102
- # ── 校验并消费 ──────────────────────────────────────
103
100
  def consume(self, session: str, nonce: str, action: str = "",
104
- args: dict = None, now: float = None) -> Tuple[bool, str]:
101
+ args: dict | None = None, now: float | None = None) -> Tuple[bool, str]:
105
102
  """校验确认码并**立即作废**。返回 (是否通过, 原因码)。
106
103
 
107
104
  原因码:``ok`` / ``missing_nonce`` / ``unknown_nonce`` / ``already_used`` /
@@ -113,8 +110,6 @@ class ConfirmationBroker:
113
110
  now = now if now is not None else self._clock()
114
111
  with self._lock:
115
112
  item: Optional[PendingConfirm] = self._items.get(nonce)
116
- # 先看这一条再清理:否则过期的码会被 _purge 抹掉,排错时只会看到
117
- # unknown_nonce,分不清"从没发过"和"发过但过期了"
118
113
  if item is not None and item.expires <= now:
119
114
  self._items.pop(nonce, None)
120
115
  return False, "expired"
@@ -130,10 +125,9 @@ class ConfirmationBroker:
130
125
  if args is not None and item.fingerprint != action_fingerprint(item.action, args):
131
126
  return False, "args_mismatch"
132
127
  item.used = True
133
- self._items.pop(nonce, None) # 单次使用:消费即删
128
+ self._items.pop(nonce, None)
134
129
  return True, "ok"
135
130
 
136
- # ── 观测 ────────────────────────────────────────────
137
131
  def pending(self, session: str = "") -> list:
138
132
  """未消费且未过期的确认码(可按会话过滤),供排查/前端展示。"""
139
133
  now = self._clock()
@@ -142,7 +136,7 @@ class ConfirmationBroker:
142
136
  return [it.to_dict() for it in self._items.values()
143
137
  if not it.used and (not session or it.session == session)]
144
138
 
145
- def purge(self, now: float = None) -> int:
139
+ def purge(self, now: float | None = None) -> int:
146
140
  now = now if now is not None else self._clock()
147
141
  with self._lock:
148
142
  return self._purge_locked(now)
@@ -37,10 +37,10 @@ def load_env_file(path: str | Path) -> List[str]:
37
37
  shadows: List[str] = []
38
38
  for key, value in (dotenv_values(target) or {}).items():
39
39
  if not key:
40
- continue # 文件里的注释行会解出 None 键
40
+ continue
41
41
  value = (value or "").strip()
42
42
  if not value:
43
- continue # 空值 = 未配置,不覆盖系统环境
43
+ continue
44
44
  current = os.environ.get(key, "").strip()
45
45
  if current and current != value:
46
46
  shadows.append(key)
@@ -16,29 +16,24 @@ from __future__ import annotations
16
16
 
17
17
  from dataclasses import dataclass
18
18
 
19
- # ── 失败类型 ────────────────────────────────────────────────────────────
20
19
  OK = "ok"
21
- TIMEOUT = "timeout" # 单工具硬超时(工具轮主动放弃)
22
- TRANSIENT = "transient" # 网络抖动/限流/5xx —— 重试有意义
23
- NOT_CONFIGURED = "not_configured" # 没配 key/服务 —— 重试一万次也没用
24
- EMPTY = "empty" # 调通了但没内容(搜不到/没召回)
25
- INVALID_ARGS = "invalid_args" # 模型给的参数不合法 —— 让模型自己改
26
- DENIED = "denied" # 策略禁用/未知工具 —— 不许绕过
27
- NEEDS_HUMAN = "needs_human" # 红线动作需确认 —— 只能人来做
28
- EXCEPTION = "exception" # 工具内部抛异常
29
- UNKNOWN = "unknown" # 认不出来:保守(不重试、交人)
30
-
31
- #: 动作
20
+ TIMEOUT = "timeout"
21
+ TRANSIENT = "transient"
22
+ NOT_CONFIGURED = "not_configured"
23
+ EMPTY = "empty"
24
+ INVALID_ARGS = "invalid_args"
25
+ DENIED = "denied"
26
+ NEEDS_HUMAN = "needs_human"
27
+ EXCEPTION = "exception"
28
+ UNKNOWN = "unknown"
29
+
32
30
  NONE = "none"
33
- RETRY = "retry" # 自动再试一次(仅只读)
34
- REPARAM = "reparam" # 让模型换参数/换查询词
35
- DEGRADE = "degrade" # 降级如实告知,不重试
36
- HAND_TO_HUMAN = "hand_to_human" # 交人
37
- STOP = "stop" # 终止,不许再试
38
-
39
- #: 特征表。**顺序敏感**:先命中先返回。顺序上的两个坑都踩过:
40
- #: · "未返回结果"(empty)不能先被 "未返回"(timeout) 吃掉;
41
- #: · "不合法"(invalid_args)要在 "拒绝"(denied) 之前,否则参数错误被当成越权。
31
+ RETRY = "retry"
32
+ REPARAM = "reparam"
33
+ DEGRADE = "degrade"
34
+ HAND_TO_HUMAN = "hand_to_human"
35
+ STOP = "stop"
36
+
42
37
  _RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
43
38
  (DENIED, ("已被策略禁用", "被权限策略拒绝", "未知工具")),
44
39
  (NEEDS_HUMAN, ("需确认的动作", "需要你确认", "请用户确认", "只能由你亲自确认")),
@@ -50,7 +45,6 @@ _RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
50
45
  (EXCEPTION, ("执行异常",)),
51
46
  )
52
47
 
53
- #: 类型 → (动作, 是否可重试, 给模型的一句硬提示)。提示写进回注文本,模型没法装作没看见。
54
48
  _ACTIONS: dict[str, tuple[str, bool, str]] = {
55
49
  TIMEOUT: (RETRY, True,
56
50
  "这一步超时未完成,已自动重试一次仍有问题。请如实说明这一步没做成,不要编造结果。"),
@@ -94,9 +88,6 @@ class Decision:
94
88
  "retryable": self.retryable, "hint": self.hint}
95
89
 
96
90
 
97
- #: 失败回注的形态标记。工具执行侧的失败文本统一以 "(" 开头(见 tool_exec 的兜底),
98
- #: 另有少数异常文本不带括号。**不能**用"含 未/失败 就算失败"这种宽松判据:
99
- #: 搜索正文里"未来""失败案例"都会误伤,把成功结果标成失败。
100
91
  _FAILURE_MARKERS = ("执行异常", "入参不合法", "被权限策略拒绝", "缺少必填")
101
92
 
102
93
 
@@ -111,7 +102,7 @@ def classify(text: str) -> str:
111
102
  """把回注文本归类:``ok`` / 具体失败类型 / ``unknown``(认不出来,保守交人)。"""
112
103
  t = (text or "").strip()
113
104
  if not t:
114
- return UNKNOWN # 空结果:不能当成功,也不确定成因
105
+ return UNKNOWN
115
106
  if not looks_like_failure(t):
116
107
  return OK
117
108
  low = t.lower()
@@ -19,15 +19,15 @@ class Gateway:
19
19
  def __init__(self, rate_per_min: int = 600, quota_per_hour: int = 20000,
20
20
  max_clients: int = 4096):
21
21
  self.rate = max(1, int(rate_per_min))
22
- self.capacity = float(self.rate) # 桶容量 = 每分钟额度(允许突发到满桶)
23
- self.refill = self.capacity / 60.0 # 令牌/秒
22
+ self.capacity = float(self.rate)
23
+ self.refill = self.capacity / 60.0
24
24
  self.quota = max(1, int(quota_per_hour))
25
25
  self.max_clients = max(64, int(max_clients))
26
26
  self._lock = threading.Lock()
27
- self._buckets: dict = {} # client -> [tokens, last_ts]
28
- self._quota: dict = {} # client -> [count, window_start]
29
- self._seen: dict = {} # client -> last_ts(用于空闲淘汰)
30
- self.rejected = 0 # 累计拒绝数(可观测)
27
+ self._buckets: dict = {}
28
+ self._quota: dict = {}
29
+ self._seen: dict = {}
30
+ self.rejected = 0
31
31
 
32
32
  def check(self, client: str):
33
33
  """判定一次请求。返回 (allowed: bool, retry_after: int, reason: str)。
@@ -37,7 +37,6 @@ class Gateway:
37
37
  self._evict_locked(now)
38
38
  self._seen[client] = now
39
39
 
40
- # ① 配额(固定 1 小时窗口)
41
40
  q = self._quota.get(client)
42
41
  if q is None or now - q[1] >= 3600.0:
43
42
  q = [0, now]
@@ -46,7 +45,6 @@ class Gateway:
46
45
  self.rejected += 1
47
46
  return False, int(3600.0 - (now - q[1])) + 1, "quota"
48
47
 
49
- # ② 令牌桶(突发限流)
50
48
  b = self._buckets.get(client)
51
49
  if b is None:
52
50
  b = [self.capacity, now]
@@ -57,7 +55,6 @@ class Gateway:
57
55
  self.rejected += 1
58
56
  return False, int((1.0 - b[0]) / self.refill) + 1, "rate"
59
57
 
60
- # 放行:扣 1 令牌 + 计 1 配额
61
58
  b[0] -= 1.0
62
59
  q[0] += 1
63
60
  return True, 0, ""
@@ -67,7 +64,6 @@ class Gateway:
67
64
  if len(self._seen) <= self.max_clients:
68
65
  return
69
66
  n_drop = len(self._seen) - self.max_clients + max(1, self.max_clients // 10)
70
- # 按 last_ts 升序取最旧的 n_drop 个
71
67
  victims = sorted(self._seen.items(), key=lambda kv: kv[1])[:n_drop]
72
68
  for c, _ in victims:
73
69
  self._seen.pop(c, None)
@@ -46,11 +46,9 @@ from . import audit_chain
46
46
  ENV_SWITCH = "FOXCLAW_GOVERNANCE_AUDIT"
47
47
  TOPIC = "governance.decision"
48
48
 
49
- # verdict 合法取值(文档用,不做硬校验——审计不该因词汇笔误崩掉主流程)
50
- VERDICTS_RULING = ("allow", "warn", "deny", "confirm") # 裁决类
51
- VERDICTS_EXECUTION = ("success", "failed") # 执行类
49
+ VERDICTS_RULING = ("allow", "warn", "deny", "confirm")
50
+ VERDICTS_EXECUTION = ("success", "failed")
52
51
 
53
- # 已知层名(同上,仅文档)
54
52
  LAYERS = ("harness", "permission", "orchestration", "skill")
55
53
 
56
54
 
@@ -71,7 +69,6 @@ class GovernanceAuditor:
71
69
  self._spine = spine
72
70
  self._path = Path(sink_path) if sink_path else None
73
71
  self._lock = threading.Lock()
74
- # 链尾 hash:首次落盘时从文件尾部补齐(重启后接着上一条链,而不是重开 GENESIS)
75
72
  self._last_hash: str | None = None
76
73
 
77
74
  @property
@@ -95,7 +92,7 @@ class GovernanceAuditor:
95
92
  "actor": actor,
96
93
  "action": action,
97
94
  "verdict": verdict,
98
- "reason": (reason or "")[:200], # 理由截断,防巨型异常串灌爆审计
95
+ "reason": (reason or "")[:200],
99
96
  "detail": detail or {},
100
97
  "ts": datetime.now(timezone.utc).isoformat(),
101
98
  }
@@ -103,7 +100,6 @@ class GovernanceAuditor:
103
100
  self._persist(rec)
104
101
  return did
105
102
 
106
- # ── 内部:两个通道,各自独立 fail-safe ─────────────────────
107
103
 
108
104
  def _emit(self, rec: dict) -> None:
109
105
  if self._spine is None:
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
27
27
  class _Record:
28
28
  key: str
29
29
  created: float
30
- result: dict = field(default_factory=dict) # 上次执行的结果(重发时原样返回)
30
+ result: dict = field(default_factory=dict)
31
31
 
32
32
 
33
33
  def make_key(domain: str, *parts: str, version: str = "v1") -> str:
@@ -52,16 +52,14 @@ class IdempotencyGuard:
52
52
  """
53
53
 
54
54
  def __init__(self, ttl_seconds: float = 3600, max_size: int = 10000,
55
- persist_path: str = None, spine=None,
55
+ persist_path: str | None = None, spine=None,
56
56
  compact_every: int = 1000):
57
57
  self.ttl = ttl_seconds
58
58
  self.max_size = max_size
59
59
  self.persist_path = persist_path
60
60
  self._store: dict[str, _Record] = {}
61
61
  self._lock = threading.RLock()
62
- # 【P2-3】审计脊柱(可空,None 时静默)
63
62
  self._spine = spine
64
- # 【P2-3】追加日志 + 定期 compact
65
63
  self._log_path = (persist_path + ".log") if persist_path else None
66
64
  self._compact_every = compact_every
67
65
  self._ops_since_compact = 0
@@ -82,7 +80,7 @@ class IdempotencyGuard:
82
80
  rec = self._store.get(key)
83
81
  return dict(rec.result) if rec else None
84
82
 
85
- def mark(self, key: str, result: dict = None):
83
+ def mark(self, key: str, result: dict | None = None):
86
84
  """标记 key 已执行,记录结果。"""
87
85
  with self._lock:
88
86
  self._store[key] = _Record(key=key, created=time.time(),
@@ -92,9 +90,9 @@ class IdempotencyGuard:
92
90
  result=self._store[key].result)
93
91
  need_compact = self._ops_since_compact >= self._compact_every
94
92
  if need_compact:
95
- self._try_compact() # 全量写盘放主锁外(见 _try_compact)
93
+ self._try_compact()
96
94
 
97
- def check_and_mark(self, key: str, result: dict = None) -> bool:
95
+ def check_and_mark(self, key: str, result: dict | None = None) -> bool:
98
96
  """原子操作:若 key 未见过 → 标记并返回 True(应执行);
99
97
  若已见过 → 返回 False(应跳过)。
100
98
 
@@ -104,7 +102,7 @@ class IdempotencyGuard:
104
102
  with self._lock:
105
103
  self._evict_expired()
106
104
  if key in self._store:
107
- return False # 已处理过,跳过
105
+ return False
108
106
  self._store[key] = _Record(key=key, created=time.time(),
109
107
  result=dict(result or {}))
110
108
  self._enforce_capacity()
@@ -128,7 +126,6 @@ class IdempotencyGuard:
128
126
  if need_compact:
129
127
  self._try_compact()
130
128
 
131
- # ── 内部:过期/容量/持久化 ──────────────────────────────
132
129
  def _evict_expired(self):
133
130
  if self.ttl <= 0:
134
131
  return
@@ -138,9 +135,6 @@ class IdempotencyGuard:
138
135
  del self._store[k]
139
136
 
140
137
  def _enforce_capacity(self):
141
- # 超容量 → 淘汰最旧的(按 created)。
142
- # 【P2-3】淘汰必须落 delete 日志:否则重启回放追加日志时,被淘汰的 key
143
- # 会"复活"(mark 日志还在,没有对应的删除记录)。锁内调用。
144
138
  if len(self._store) <= self.max_size:
145
139
  return
146
140
  items = sorted(self._store.items(), key=lambda kv: kv[1].created)
@@ -148,7 +142,6 @@ class IdempotencyGuard:
148
142
  del self._store[k]
149
143
  self._persist_op("delete", k)
150
144
 
151
- # ── 内部:持久化(追加日志 + 定期 compact)─────────────────
152
145
  def _persist_op(self, event: str, key: str, **fields):
153
146
  """【P2-3】追加一条日志(O(1) 写,不写全量)。锁内调用(append 很快,
154
147
  且在锁内可避免并发写同一文件交错)。失败计数 + 发事件,不影响主流程。"""
@@ -196,11 +189,10 @@ class IdempotencyGuard:
196
189
  for k, r in self._store.items()
197
190
  if self.ttl <= 0 or now - r.created <= self.ttl}
198
191
  self._ops_since_compact = 0
199
- # ── 以下为主锁外的慢 IO ──
200
192
  tmp = self.persist_path + ".tmp"
201
193
  with open(tmp, "w", encoding="utf-8") as f:
202
194
  json.dump(snapshot, f, ensure_ascii=False)
203
- os.replace(tmp, self.persist_path) # 原子替换,防写一半损坏
195
+ os.replace(tmp, self.persist_path)
204
196
  if rotated and os.path.exists(rotated):
205
197
  os.remove(rotated)
206
198
  except Exception:
@@ -222,7 +214,7 @@ class IdempotencyGuard:
222
214
  tmp = self.persist_path + ".tmp"
223
215
  with open(tmp, "w", encoding="utf-8") as f:
224
216
  json.dump(data, f, ensure_ascii=False)
225
- os.replace(tmp, self.persist_path) # 原子替换,防写一半损坏
217
+ os.replace(tmp, self.persist_path)
226
218
  if self._log_path and os.path.exists(self._log_path):
227
219
  os.remove(self._log_path)
228
220
  self._ops_since_compact = 0
@@ -254,8 +246,9 @@ class IdempotencyGuard:
254
246
  一份全量快照,失去"追加日志 O(1)"的意义。"""
255
247
  try:
256
248
  now = time.time()
257
- if os.path.exists(self.persist_path):
258
- with open(self.persist_path, encoding="utf-8") as f:
249
+ snapshot = self.persist_path
250
+ if snapshot is not None and os.path.exists(snapshot):
251
+ with open(snapshot, encoding="utf-8") as f:
259
252
  data = json.load(f)
260
253
  for k, v in data.items():
261
254
  created = v.get("created", now)
@@ -272,18 +265,18 @@ class IdempotencyGuard:
272
265
  try:
273
266
  ev = json.loads(line)
274
267
  except Exception:
275
- continue # 跳过损坏行(append 末行可能写一半)
268
+ continue
276
269
  self._apply_log_event(ev, now)
277
270
  replayed = True
278
271
  if replayed:
279
272
  self._compact()
280
273
  except Exception:
281
- pass # 加载失败 → 从空开始,不影响启动
274
+ pass
282
275
 
283
276
  def stats(self) -> dict:
284
277
  with self._lock:
285
278
  self._evict_expired()
286
279
  return {"size": len(self._store), "max_size": self.max_size,
287
280
  "ttl_seconds": self.ttl, "persisted": bool(self.persist_path),
288
- "persist_failures": self._persist_failures, # 【P2-3】
289
- "ops_since_compact": self._ops_since_compact} # 【P2-3】
281
+ "persist_failures": self._persist_failures,
282
+ "ops_since_compact": self._ops_since_compact}
@@ -18,7 +18,6 @@ import os
18
18
  import re
19
19
  import secrets
20
20
 
21
- # 典型注入/越权特征(中英)。命中不拦截,只在数据块里加醒目告警,提示模型"这是数据里的指令,别执行"。
22
21
  _MARKERS = [
23
22
  r"ignore\s+(all\s+)?(the\s+)?previous\s+instructions?",
24
23
  r"disregard\s+(the\s+)?(above|previous|prior)",
@@ -42,25 +41,17 @@ def scan(text: str) -> bool:
42
41
  return bool(text) and bool(_RE.search(str(text)))
43
42
 
44
43
 
45
- # ── 输出侧护栏(纵深防御第四道:DLP)────────────────────────────────────────────
46
- # 回复返回/落库前扫一道:① 密钥/令牌等敏感串 → 脱敏;② 系统提示词被整段吐出 → 拦截。
47
- # 纯正则、零额外模型调用(适配 flash 成本)。与"输入-隔离-模型"三层凑齐四道墙。
48
-
49
- # 常见密钥/令牌形态(含 Foxclaw 实际用的百度千帆 bce-v3 AppBuilder key)。
50
44
  _SECRET_PATTERNS = [
51
- r"sk-[A-Za-z0-9]{20,}", # OpenAI 风格
52
- r"bce-v3/[A-Za-z0-9-]{6,}/[A-Za-z0-9]{6,}", # 百度千帆 AppBuilder(Foxclaw 在用)
53
- r"AKIA[0-9A-Z]{16}", # AWS Access Key
54
- r"gh[pousr]_[A-Za-z0-9]{20,}", # GitHub token
55
- r"xox[baprs]-[A-Za-z0-9-]{10,}", # Slack token
56
- r"Bearer\s+[A-Za-z0-9._\-]{20,}", # Bearer 令牌
45
+ r"sk-[A-Za-z0-9]{20,}",
46
+ r"bce-v3/[A-Za-z0-9-]{6,}/[A-Za-z0-9]{6,}",
47
+ r"AKIA[0-9A-Z]{16}",
48
+ r"gh[pousr]_[A-Za-z0-9]{20,}",
49
+ r"xox[baprs]-[A-Za-z0-9-]{10,}",
50
+ r"Bearer\s+[A-Za-z0-9._\-]{20,}",
57
51
  r"(api[_-]?key|secret|token|password|passwd)\s*[:=]\s*['\"]?[A-Za-z0-9._\-/]{12,}",
58
52
  ]
59
53
  _SECRET_RE = re.compile("|".join(_SECRET_PATTERNS), re.IGNORECASE)
60
54
 
61
- # 系统提示词泄露判定:不用"单个短语命中"(会误伤——如用户正常问"Principle 07 是什么")。
62
- # 改为**更强信号**:① 输出里出现 KERNEL 的长段**逐字串**(dump 才会逐字复现,提及概念不会);
63
- # ② 或同时出现 ≥2 个**章节标记**(无 kernel 参照时的兜底)。
64
55
  _SECTION_MARKS = ("【一、", "【二、", "【三、", "【四、", "【五、", "【六、", "【七、")
65
56
 
66
57
 
@@ -101,7 +92,6 @@ def _has_semantic_leak(text: str, kernel: str, threshold: float = 0.88,
101
92
  if model is None:
102
93
  return False
103
94
  import numpy as np
104
- # 固定 ~200 字块;向量已 normalize → 余弦 = 点积。内核块进共享缓存(只编码一次)。
105
95
  kblocks = [kernel[i:i + 200] for i in range(0, len(kernel), 200)]
106
96
  tblocks = [text[i:i + 200] for i in range(0, len(text), 200)]
107
97
  kv = encode_cached(model, name, kblocks)
@@ -165,8 +155,6 @@ def guard_output(text: str, kernel: str = "") -> tuple:
165
155
  return (text, issues)
166
156
 
167
157
 
168
- # 分隔符伪造防护:外部内容里若自带 <untrusted>/</untrusted>,会伪造边界把恶意指令挤出隔离区
169
- # (经典分隔符注入)。包裹前把内容中一切 <(/)untrusted 的尖括号中和成 ‹,使其无法充当真边界。
170
158
  _UNTRUSTED_DELIM = re.compile(r"<\s*/?\s*untrusted", re.IGNORECASE)
171
159
 
172
160
 
@@ -196,17 +184,10 @@ def wrap_untrusted(text: str, source: str = "external") -> str:
196
184
  return block + note
197
185
 
198
186
 
199
- # ── 数据污点分级(Muse「eBPF 污点追踪」的纯软件版)────────────────────────────
200
- # 对齐 Meta Muse 的安全内核:不靠"模型自觉不泄露",而靠"数据一旦来自用户/敏感源,
201
- # 就在会话上打污点;之后凡外发/副作用动作,污点达标则动态提高审批门槛(见
202
- # skills.permission 的 taint_threshold)。这里只做「来源 → 敏感度」映射;污点的
203
- # 传播与裁决在调用方(agents.base / skills.permission)。
204
- #
205
- # 敏感度分级(数值越高越敏感,单调可比):
206
- TAINT_NONE = 0 # 无外部数据(会话初始态)
207
- TAINT_EXTERNAL = 1 # 外部不可控但非个人:web_search 结果
208
- TAINT_PERSONAL = 2 # 用户个人数据:知识库(knowledge)/文件(file)
209
- TAINT_SENSITIVE = 3 # 高敏感个人数据:个人记忆(memory)
187
+ TAINT_NONE = 0
188
+ TAINT_EXTERNAL = 1
189
+ TAINT_PERSONAL = 2
190
+ TAINT_SENSITIVE = 3
210
191
 
211
192
  _TAINT_BY_SOURCE = {
212
193
  "web_search": TAINT_EXTERNAL,