foxclaw 0.1.2 → 0.1.4

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 (368) 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/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. 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:
@@ -0,0 +1,283 @@
1
+ """高敏感记忆写入的同意闸门 (SensitiveWritePolicy)。
2
+
3
+ 问题
4
+ ────
5
+ `memory_adapter.ingest` 此前对「写什么内容」不做任何判定:凭据、证件号、诊断结论
6
+ 一旦被抽进长期记忆,就会在之后每次召回里反复出现在上下文中,而且会随 prompt 发往云端
7
+ 模型。spec 的 `confirm_high_sensitivity` 要求这类内容落库前得到用户显式同意。
8
+
9
+ 判定必须是确定性的
10
+ ──────────────────
11
+ 不用 LLM 判定敏感度:写入路径再调一次模型既贵,结果又不可复现,同一句话今天拦明天放,
12
+ 用户无法建立预期。这里只用正则 + Luhn 校验,命中即命中,可单测。
13
+
14
+ 误判方向是刻意的
15
+ ────────────────
16
+ 漏判 = 该拦的不拦,等于没做;误判 = 用户多点一次确认。所以宁可多拦:「我最近焦虑」
17
+ 也会被拦一次。反过来,财务 Agent 天天谈金额,因此 finance 类**只匹配卡号/账户号**,
18
+ 绝不匹配普通金额 —— 否则功能一开就无法使用。
19
+
20
+ 三档模式 (FOXCLAW_SENSITIVE_WRITE_MODE)
21
+ ──────────────────────────────────────
22
+ audit (默认) 只记录不拦截。落库照常,事件里标 allowed。
23
+ warn 记录 + 进程内告警,仍不落 pending。
24
+ enforce 真正拦截:高敏感内容不进长期记忆,转成待确认项,用户同意后才落库。
25
+
26
+ 为什么默认 audit
27
+ ────────────────
28
+ 分类规则在真实流量上跑通之前只是**待验证的假设**。先在 audit 下看清命中长什么样、
29
+ 误判率多少,再切 enforce —— 与 shared_write 的灰度立场一致。
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import os
35
+ import re
36
+ import threading
37
+ import uuid
38
+ from dataclasses import dataclass
39
+ from pathlib import Path
40
+ from typing import Any, Iterable
41
+
42
+ MODE_ENV = "FOXCLAW_SENSITIVE_WRITE_MODE"
43
+ VALID_MODES = ("audit", "warn", "enforce")
44
+
45
+ PENDING_FILE = "sensitive_pending.json"
46
+ MAX_PENDING = 200
47
+
48
+
49
+ def _mask(s: str) -> str:
50
+ """打码后才允许进事件/日志 —— 审计出口本身不能变成泄露点。"""
51
+ s = str(s or "")
52
+ if len(s) <= 6:
53
+ return "*" * len(s)
54
+ return f"{s[:2]}{'*' * (len(s) - 4)}{s[-2:]}"
55
+
56
+
57
+ def _luhn_ok(digits: str) -> bool:
58
+ total, alt = 0, False
59
+ for ch in reversed(digits):
60
+ if not ch.isdigit():
61
+ return False
62
+ d = int(ch)
63
+ if alt:
64
+ d *= 2
65
+ if d > 9:
66
+ d -= 9
67
+ total += d
68
+ alt = not alt
69
+ return total % 10 == 0
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class Hit:
74
+ category: str
75
+ pattern: str
76
+ sample: str
77
+
78
+ def to_dict(self) -> dict:
79
+ return {"category": self.category, "pattern": self.pattern, "sample": self.sample}
80
+
81
+
82
+ # 每个模式都是「有上下文锚点」的,不裸匹配长数字 —— 裸匹配会把订单号、时间戳全卷进来。
83
+ _RULES: dict[str, tuple[str, ...]] = {
84
+ "credential": (
85
+ r"(?:密码|口令|password|passwd|pwd)\s*[::=是为]\s*\S{3,}",
86
+ r"(?:api[_-]?key|secret|token|access[_-]?key)\s*[::=]\s*[A-Za-z0-9_\-]{12,}",
87
+ r"\bsk-[A-Za-z0-9]{16,}",
88
+ r"\bghp_[A-Za-z0-9]{20,}",
89
+ r"\bAKIA[0-9A-Z]{16}\b",
90
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
91
+ ),
92
+ "identity": (
93
+ r"\b[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b",
94
+ r"(?:身份证|护照|港澳通行证|社保卡|驾驶证)[号编]?\s*[::]?\s*[A-Za-z0-9]{6,}",
95
+ ),
96
+ "health": (
97
+ r"(?:确诊|诊断为|病历|住院|手术|处方|服药|用药|过敏史|既往史)",
98
+ r"(?:抑郁症|焦虑症|糖尿病|高血压|心脏病|癌|HIV|乙肝)",
99
+ ),
100
+ "finance_detail": (
101
+ r"(?:银行卡|账户|卡号|账号)\s*[::]?\s*\d{6,}",
102
+ r"(?:CVV|CVC|安全码|有效期)\s*[::]?\s*\d{3,4}",
103
+ r"\b(?:信用卡|储蓄卡|借记卡)\b",
104
+ ),
105
+ }
106
+
107
+ _COMPILED = {cat: [re.compile(p) for p in pats] for cat, pats in _RULES.items()}
108
+
109
+ # 卡号单独走一次 Luhn:16-19 位裸数字只有校验通过才算,避免误伤订单号/流水号。
110
+ _CARD_CANDIDATE = re.compile(r"\b(?:\d[ -]?){15,18}\d\b")
111
+
112
+
113
+ class SensitivityClassifier:
114
+ """确定性判定:返回命中的类别与打码后的样本。无状态,并发安全。"""
115
+
116
+ def __init__(self, categories: Iterable[str] | None = None):
117
+ allowed = set(categories) if categories is not None else set(_COMPILED)
118
+ self.categories = tuple(c for c in _COMPILED if c in allowed)
119
+
120
+ def classify(self, content: str) -> tuple[Hit, ...]:
121
+ text = str(content or "")
122
+ if not text:
123
+ return ()
124
+ hits: list[Hit] = []
125
+ for cat in self.categories:
126
+ for rx in _COMPILED[cat]:
127
+ m = rx.search(text)
128
+ if m:
129
+ hits.append(Hit(cat, rx.pattern, _mask(m.group(0))))
130
+ break
131
+ if "finance_detail" in self.categories:
132
+ for m in _CARD_CANDIDATE.finditer(text):
133
+ digits = re.sub(r"\D", "", m.group(0))
134
+ if len(digits) >= 16 and _luhn_ok(digits):
135
+ hits.append(Hit("finance_detail", "luhn", _mask(digits)))
136
+ break
137
+ return tuple(hits)
138
+
139
+
140
+ @dataclass
141
+ class Decision:
142
+ allowed: bool
143
+ reason: str
144
+ mode: str
145
+ categories: tuple = ()
146
+ hits: tuple = ()
147
+ pending_id: str = ""
148
+
149
+ @property
150
+ def is_pending(self) -> bool:
151
+ return bool(self.pending_id)
152
+
153
+ def to_dict(self) -> dict:
154
+ return {
155
+ "allowed": self.allowed, "reason": self.reason, "mode": self.mode,
156
+ "categories": list(self.categories),
157
+ "hits": [h.to_dict() for h in self.hits],
158
+ "pending_id": self.pending_id,
159
+ }
160
+
161
+
162
+ class PendingStore:
163
+ """待确认的高敏感写入。落盘是因为写入可能跨进程/重启发生,内存队列会静默丢内容。"""
164
+
165
+ def __init__(self, path: str | Path):
166
+ self.path = Path(path)
167
+ self._lock = threading.RLock()
168
+
169
+ def _read(self) -> list:
170
+ try:
171
+ data = json.loads(self.path.read_text(encoding="utf-8"))
172
+ except Exception:
173
+ return []
174
+ return data if isinstance(data, list) else []
175
+
176
+ def _write(self, items: list) -> None:
177
+ self.path.parent.mkdir(parents=True, exist_ok=True)
178
+ tmp = self.path.with_suffix(".tmp")
179
+ tmp.write_text(json.dumps(items, ensure_ascii=False, indent=1), encoding="utf-8")
180
+ os.replace(tmp, self.path)
181
+
182
+ def add(self, entry: dict) -> str:
183
+ with self._lock:
184
+ items = self._read()
185
+ pid = uuid.uuid4().hex[:12]
186
+ items.append({"id": pid, **entry})
187
+ self._write(items[-MAX_PENDING:])
188
+ return pid
189
+
190
+ def list(self) -> list:
191
+ with self._lock:
192
+ return self._read()
193
+
194
+ def pop(self, pid: str) -> dict | None:
195
+ with self._lock:
196
+ items = self._read()
197
+ kept = [it for it in items if it.get("id") != pid]
198
+ found = next((it for it in items if it.get("id") == pid), None)
199
+ if found is not None:
200
+ self._write(kept)
201
+ return found
202
+
203
+
204
+ class SensitiveWritePolicy:
205
+ """判定「这条内容能否直接写入长期记忆」。纯函数式判定 + 挂载式装配(同 shared_write)。"""
206
+
207
+ def __init__(self, mode: str | None = None,
208
+ classifier: SensitivityClassifier | None = None,
209
+ pending_path: str | Path | None = None):
210
+ self.mode = self._resolve_mode(mode)
211
+ self.classifier = classifier or SensitivityClassifier()
212
+ self.pending = PendingStore(pending_path or _default_pending_path())
213
+
214
+ @staticmethod
215
+ def _resolve_mode(mode: str | None = None) -> str:
216
+ m = (mode or os.environ.get(MODE_ENV, "") or "audit").strip().lower()
217
+ return m if m in VALID_MODES else "audit"
218
+
219
+ def evaluate(self, content: str, **entry: Any) -> Decision:
220
+ hits = self.classifier.classify(content)
221
+ if not hits:
222
+ return Decision(True, "无高敏感特征", self.mode)
223
+ cats = tuple(dict.fromkeys(h.category for h in hits))
224
+ if self.mode == "enforce":
225
+ return Decision(False, "高敏感内容需用户同意后才能写入长期记忆",
226
+ self.mode, cats, hits)
227
+ label = "audit 不拦截" if self.mode == "audit" else "warn 不拦截"
228
+ return Decision(True, f"高敏感特征({label})", self.mode, cats, hits)
229
+
230
+ def stash(self, content: str, entry: dict, decision: Decision) -> Decision:
231
+ """enforce 下把被拦的写入转成待确认项,返回带 pending_id 的 Decision。"""
232
+ pid = self.pending.add({**entry, "content": str(content or ""),
233
+ "categories": list(decision.categories)})
234
+ return Decision(decision.allowed, decision.reason, decision.mode,
235
+ decision.categories, decision.hits, pid)
236
+
237
+
238
+ def _default_pending_path() -> Path:
239
+ root = os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data"
240
+ return Path(root) / PENDING_FILE
241
+
242
+
243
+ def guard_sensitive_write(backend, agent_id: str | None, content: str,
244
+ memory_type: str = "concept", **entry: Any) -> Decision | None:
245
+ """记忆落库前的统一闸门。未挂策略时返回 None(向后兼容,与 guard_shared_write 一致)。
246
+
247
+ enforce 且命中高敏感 → 返回 allowed=False 的 Decision,调用方**必须**据此跳过落库;
248
+ 被拦内容此时已进入待确认队列,不会丢。
249
+ """
250
+ policy = getattr(backend, "sensitive_write_policy", None)
251
+ if policy is None:
252
+ return None
253
+
254
+ decision = policy.evaluate(content, **entry)
255
+ spine = getattr(backend, "sensitive_write_spine", None)
256
+ if spine is not None:
257
+ try:
258
+ spine.publish("memory.sensitive", {
259
+ "agent_id": agent_id or "",
260
+ "type": memory_type,
261
+ "allowed": decision.allowed,
262
+ "mode": decision.mode,
263
+ "categories": list(decision.categories),
264
+ "hits": [_mask(h.sample) for h in decision.hits],
265
+ }, source=agent_id or "memory")
266
+ except Exception:
267
+ pass
268
+
269
+ if not decision.allowed:
270
+ if policy.mode == "enforce":
271
+ return policy.stash(content, {"agent_id": agent_id or "",
272
+ "type": memory_type}, decision)
273
+ if policy.mode == "warn":
274
+ print(f"[Foxclaw] ⚠ 高敏感记忆写入(未拦截): agent={agent_id!r} "
275
+ f"categories={list(decision.categories)}")
276
+ return decision
277
+
278
+
279
+ def attach(backend, policy: SensitiveWritePolicy | None = None, spine=None):
280
+ """挂到记忆后端。与 shared_write.attach 同因:装配顺序上脊柱晚于 memory_adapter。"""
281
+ backend.sensitive_write_policy = policy or SensitiveWritePolicy()
282
+ backend.sensitive_write_spine = spine
283
+ return backend.sensitive_write_policy
@@ -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()