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
@@ -18,20 +18,20 @@ from collections import deque
18
18
  from dataclasses import dataclass
19
19
  from typing import Callable, Optional, Literal
20
20
 
21
+ from .hooks_spec import HookEvent
21
22
 
22
- # ============ 断路器 ============
23
23
 
24
24
  CircuitState = Literal["closed", "open", "half_open"]
25
25
 
26
26
 
27
27
  @dataclass
28
28
  class _BreakerEntry:
29
- failures: int = 0 # closed 态的连续失败计数
29
+ failures: int = 0
30
30
  state: CircuitState = "closed"
31
- opened_at: float = 0.0 # 进入 open 的时刻(用于冷却计时)
32
- probe_in_flight: bool = False # half_open 态:是否已有探针在执行(只放一个)
33
- probe_started_at: float = 0.0 # 探针放行的时刻(用于探针超时自愈)
34
- probe_successes: int = 0 # half_open 态:已累计的探针成功数
31
+ opened_at: float = 0.0
32
+ probe_in_flight: bool = False
33
+ probe_started_at: float = 0.0
34
+ probe_successes: int = 0
35
35
 
36
36
 
37
37
  class CircuitBreaker:
@@ -48,10 +48,10 @@ class CircuitBreaker:
48
48
  recovery_threshold: int = 1, probe_timeout: float = 60.0):
49
49
  self.threshold = threshold
50
50
  self.cooldown = cooldown
51
- self.recovery_threshold = max(1, recovery_threshold) # 恢复需连续成功几次
52
- self.probe_timeout = probe_timeout # 探针超过此时长未记账 → 视为丢失,释放
51
+ self.recovery_threshold = max(1, recovery_threshold)
52
+ self.probe_timeout = probe_timeout
53
53
  self._entries: dict[str, _BreakerEntry] = {}
54
- self._lock = threading.RLock() # allow/record 可能跨线程,需加锁
54
+ self._lock = threading.RLock()
55
55
 
56
56
  def _get(self, sid: str) -> _BreakerEntry:
57
57
  if sid not in self._entries:
@@ -71,17 +71,14 @@ class CircuitBreaker:
71
71
  return True
72
72
  if e.state == "open":
73
73
  if time.time() - e.opened_at >= self.cooldown:
74
- # 冷却满 → 真正进入 half_open,放行第一个探针
75
74
  e.state = "half_open"
76
75
  e.probe_in_flight = True
77
76
  e.probe_started_at = time.time()
78
77
  e.probe_successes = 0
79
78
  return True
80
79
  return False
81
- # half_open:已有探针在飞 → 拒绝其余;否则放行一个新探针
82
80
  if e.state == "half_open":
83
81
  if e.probe_in_flight:
84
- # 探针超时未记账(调用方崩溃/漏记)→ 视为丢失,释放以免永久卡死
85
82
  if time.time() - e.probe_started_at >= self.probe_timeout:
86
83
  e.probe_in_flight = False
87
84
  else:
@@ -98,11 +95,9 @@ class CircuitBreaker:
98
95
  e.probe_in_flight = False
99
96
  e.probe_successes += 1
100
97
  if e.probe_successes >= self.recovery_threshold:
101
- # 探针连续成功达标 → 恢复
102
98
  e.state = "closed"
103
99
  e.failures = 0
104
100
  e.probe_successes = 0
105
- # 否则保持 half_open,等下一个探针
106
101
  else:
107
102
  e.failures = 0
108
103
  e.state = "closed"
@@ -111,7 +106,6 @@ class CircuitBreaker:
111
106
  with self._lock:
112
107
  e = self._get(sid)
113
108
  if e.state == "half_open":
114
- # 探针失败 → 回到 open,重新计冷却
115
109
  e.state = "open"
116
110
  e.opened_at = time.time()
117
111
  e.probe_in_flight = False
@@ -126,7 +120,6 @@ class CircuitBreaker:
126
120
  with self._lock:
127
121
  e = self._get(sid)
128
122
  state = e.state
129
- # open 但冷却已满 → 对外显示 half_open(下次 allow 会真正转入)
130
123
  if e.state == "open" and time.time() - e.opened_at >= self.cooldown:
131
124
  state = "half_open"
132
125
  return {"state": state, "failures": e.failures,
@@ -154,20 +147,11 @@ class CircuitBreaker:
154
147
  return False
155
148
 
156
149
 
157
- # ============ 生命周期钩子 ============
158
-
159
- HookEvent = Literal["before_tool_call", "after_tool_call", "on_error", "on_loop",
160
- "session_start", "agent_start", "agent_end", "session_end",
161
- "before_message_write",
162
- # 向后兼容的旧名 + E2 语义别名(内部规整为标准名)
163
- "pre_skill", "post_skill", "pre_reasoning", "task_complete"]
164
-
165
-
166
150
  @dataclass
167
151
  class Hook:
168
152
  event: HookEvent
169
- matcher: str # skill id / glob;'*' 匹配全部
170
- handler: Callable # 宿主提供的处理函数
153
+ matcher: str
154
+ handler: Callable
171
155
  name: str = ""
172
156
 
173
157
  def matches(self, skill_id: str) -> bool:
@@ -176,10 +160,10 @@ class Hook:
176
160
 
177
161
  @dataclass
178
162
  class HookResult:
179
- blocked: bool = False # 仅 pre_skill 可阻断
163
+ blocked: bool = False
180
164
  reason: str = ""
181
165
  modified_args: Optional[dict] = None
182
- additional_context: Optional[str] = None # E2:CONTEXT_HOOKS(pre_reasoning/agent_start)注入的推理上下文
166
+ additional_context: Optional[str] = None
183
167
 
184
168
 
185
169
  class HookManager:
@@ -195,18 +179,13 @@ class HookManager:
195
179
  def __init__(self, timeout: float = 10.0, audit_limit: int = 2000):
196
180
  self.timeout = timeout
197
181
  self._hooks: list[Hook] = []
198
- # 【深审修复·长期运行泄漏】audit_log 原为普通 list、只增不减:每次 skill invoke 触发
199
- # pre/post/error/loop 等钩子都 append,长跑(尤其 V2 管线内 dispatch 高频用工具)无界
200
- # 增长 → 内存泄漏(与 TruthCrystallizer._pending 同类)。改有界 deque:保留最近 N 条,
201
- # 溢出自动丢弃最老的;deque.append 原子 ⇒ 并发 fire 也线程安全,无需额外锁。
202
182
  self.audit_log: deque = deque(maxlen=max(100, audit_limit))
203
183
 
204
184
  def register(self, event: HookEvent, handler: Callable,
205
185
  matcher: str = "*", name: str = ""):
206
186
  from .hooks_spec import normalize_hook, is_valid_hook
207
- norm = normalize_hook(event) # 旧名(pre_skill)→ 标准名(before_tool_call)
187
+ norm = normalize_hook(event)
208
188
  if not is_valid_hook(norm):
209
- # 非法钩子名(可能拼写错误)→ 记录警告,避免静默失效难以排查
210
189
  self.audit_log.append({
211
190
  "event": event, "hook": name or getattr(handler, "__name__", "?"),
212
191
  "matcher": matcher, "duration_ms": 0, "timed_out": False,
@@ -225,7 +204,6 @@ class HookManager:
225
204
  except Exception as e:
226
205
  err = str(e)
227
206
  dt = time.time() - t0
228
- # 超时只记录(同步 hook 无法真正中断,但记录超时供审计)
229
207
  timed_out = dt > self.timeout
230
208
  self.audit_log.append({
231
209
  "event": hook.event, "hook": hook.name, "matcher": hook.matcher,
@@ -234,7 +212,7 @@ class HookManager:
234
212
  })
235
213
  return {} if err else outcome
236
214
 
237
- def fire(self, event: HookEvent, skill_id: str = "", ctx: dict = None) -> HookResult:
215
+ def fire(self, event: HookEvent, skill_id: str = "", ctx: dict | None = None) -> HookResult:
238
216
  """触发某事件的所有匹配 hook。仅 before_tool_call(原 pre_skill)的 block 生效;
239
217
  CONTEXT_HOOKS(pre_reasoning/agent_start)返回的 additional_context 会被收集拼接。
240
218
 
@@ -245,18 +223,16 @@ class HookManager:
245
223
  """
246
224
  from .hooks_spec import (normalize_hook, BLOCKING_HOOKS, TOOL_SCOPED_HOOKS,
247
225
  CONTEXT_HOOKS)
248
- event = normalize_hook(event) # 旧名/标准名统一
226
+ event = normalize_hook(event)
249
227
  ctx = dict(ctx or {}, event=event, skill_id=skill_id)
250
228
  result = HookResult()
251
229
  ctx_parts = []
252
230
  for hook in self._hooks:
253
231
  if hook.event != event:
254
232
  continue
255
- # 工具级钩子按 skill_id 过滤(matcher)
256
233
  if event in TOOL_SCOPED_HOOKS and not hook.matches(skill_id):
257
234
  continue
258
235
  out = self._run_one(hook, ctx)
259
- # 仅有阻断权的钩子(before_tool_call)的 block / modified_args 生效
260
236
  if event in BLOCKING_HOOKS:
261
237
  if out.get("block"):
262
238
  result.blocked = True
@@ -264,7 +240,6 @@ class HookManager:
264
240
  return result
265
241
  if out.get("modified_args") is not None:
266
242
  result.modified_args = out["modified_args"]
267
- # 可注入上下文的钩子:收集 additional_context(兼容驼峰 additionalContext)
268
243
  if event in CONTEXT_HOOKS:
269
244
  inj = out.get("additional_context") or out.get("additionalContext")
270
245
  if inj:
@@ -274,5 +249,4 @@ class HookManager:
274
249
  return result
275
250
 
276
251
  def recent_audit(self, n: int = 20) -> list:
277
- # deque 不支持负索引切片,先快照成 list(原子快照,并发安全)再取尾 N 条
278
252
  return list(self.audit_log)[-n:]
@@ -4,6 +4,7 @@ import re
4
4
  import threading
5
5
  from dataclasses import dataclass, field, asdict
6
6
  import yaml
7
+ from typing import Any
7
8
 
8
9
 
9
10
  @dataclass
@@ -15,11 +16,11 @@ class MarkdownSkill:
15
16
  body: str = ""
16
17
  enabled: bool = True
17
18
  source: str = "user"
18
- version: str = "" # OpenClaw 包的版本(_meta.json/frontmatter),可空
19
- builtin: bool = False # 随包分发的内置技能(repo skills/*/SKILL.md)
19
+ version: str = ""
20
+ builtin: bool = False
20
21
 
21
22
  def to_dict(self) -> dict:
22
- d = asdict(self); d["trust"] = "low"; return d # 指令注入=提示词操纵面,信任=低
23
+ d = asdict(self); d["trust"] = "low"; return d
23
24
 
24
25
  def to_meta(self) -> dict:
25
26
  d = self.to_dict(); d.pop("body", None); return d
@@ -29,7 +30,7 @@ def _slug(s: str) -> str:
29
30
  return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_") or "skill"
30
31
 
31
32
 
32
- def parse_skill_md(text: str, id: str = None) -> MarkdownSkill:
33
+ def parse_skill_md(text: str, id: str | None = None) -> MarkdownSkill:
33
34
  """解析 SKILL.md(--- yaml frontmatter --- + 正文)。缺 name/description → ValueError。"""
34
35
  text = (text or "").lstrip("").strip()
35
36
  m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
@@ -65,7 +66,7 @@ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
65
66
  用户已安装的同 id 技能优先(不覆盖),内置只是缺省值。
66
67
  """
67
68
  from pathlib import Path
68
- loaded = []
69
+ loaded: list[Any] = []
69
70
  try:
70
71
  base = Path(root)
71
72
  if not base.is_dir():
@@ -74,7 +75,7 @@ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
74
75
  try:
75
76
  s = parse_skill_md(p.read_text(encoding="utf-8", errors="replace"))
76
77
  except Exception:
77
- continue # 单个技能坏掉不影响其余
78
+ continue
78
79
  if not s.builtin or reg.get(s.id) is not None:
79
80
  continue
80
81
  reg.register(s)
@@ -88,7 +89,7 @@ class MarkdownSkillRegistry:
88
89
  def __init__(self):
89
90
  self._skills = {}
90
91
  self._lock = threading.RLock()
91
- self.policy = None # 全局封杀策略(宿主注入);被封的不参与注入
92
+ self.policy = None
92
93
 
93
94
  def register(self, s: MarkdownSkill):
94
95
  with self._lock:
@@ -119,7 +120,7 @@ class MarkdownSkillRegistry:
119
120
  if not s.enabled:
120
121
  continue
121
122
  if pol is not None and pol.is_blocked(s.id):
122
- continue # 被封杀 → 不注入(kill-switch)
123
+ continue
123
124
  if s.triggers:
124
125
  if any(k.lower() in t_low for k in s.triggers):
125
126
  out.append(s)
@@ -164,7 +165,7 @@ def extract_skill_package(zip_bytes: bytes, max_uncompressed: int = 2_000_000):
164
165
  if not skill_name:
165
166
  raise ValueError("包内未找到 SKILL.md")
166
167
  skill_md = zf.read(skill_name).decode("utf-8", "replace")
167
- meta = {}
168
+ meta: dict[str, Any] = {}
168
169
  meta_name = next((n for n in zf.namelist() if n.lower().endswith("_meta.json")), None)
169
170
  if meta_name:
170
171
  try:
@@ -11,7 +11,6 @@ from mcp.client.streamable_http import streamablehttp_client
11
11
  from mcp.client.sse import sse_client
12
12
 
13
13
 
14
- # ── async→sync 桥:单例后台 loop 线程 ──────────────────────────
15
14
  class _Bridge:
16
15
  def __init__(self):
17
16
  self._loop = asyncio.new_event_loop()
@@ -27,7 +26,7 @@ class _Bridge:
27
26
  try:
28
27
  return fut.result(timeout=timeout)
29
28
  except BaseException:
30
- fut.cancel() # 超时/异常必须取消协程,否则挂起的连接会一直留在 loop 里泄漏
29
+ fut.cancel()
31
30
  raise
32
31
 
33
32
 
@@ -43,7 +42,6 @@ def _get_bridge() -> _Bridge:
43
42
  return _bridge
44
43
 
45
44
 
46
- # ── SSRF 防线:连接(含重定向的每一跳)都校验目标 host 不是内网/本地/元数据 ──────
47
45
  async def _ssrf_request_hook(request):
48
46
  """httpx 请求钩子:对每个即将发出的请求(初始 + 重定向 follow-up)校验 host。
49
47
  堵 redirect-SSRF + DNS 重绑定 + 配置漂移(恶意 server 把你导去内网/云元数据)。"""
@@ -59,12 +57,11 @@ def _safe_httpx_factory(headers=None, timeout=None, auth=None):
59
57
  headers=headers,
60
58
  timeout=timeout if timeout is not None else httpx.Timeout(30.0),
61
59
  auth=auth,
62
- follow_redirects=True, # 允许(合法同源尾斜杠跳转),但每跳过钩子
60
+ follow_redirects=True,
63
61
  event_hooks={"request": [_ssrf_request_hook]},
64
62
  )
65
63
 
66
64
 
67
- # ── 底层 async 助手(测试可 monkeypatch 这两个) ─────────────────
68
65
  @asynccontextmanager
69
66
  async def _connect(url, headers, transport):
70
67
  if transport == "sse":
@@ -98,14 +95,13 @@ def _norm(cfg: dict) -> dict:
98
95
  "transport": cfg.get("transport") or "streamable_http"}
99
96
 
100
97
 
101
- # ── 工具 → SkillSpec(fail-closed) ─────────────────────────────
102
98
  def _get(tool, key, default=None):
103
99
  """兼容 dict(守门人序列化后) 与 mcp.types.Tool 对象(直连回退路径)。"""
104
100
  return tool.get(key, default) if isinstance(tool, dict) else getattr(tool, key, default)
105
101
 
106
102
 
107
103
  def tool_to_spec(server_id: str, tool):
108
- from . import SkillSpec
104
+ from . import SideEffect, SkillSpec
109
105
  ann = _get(tool, "annotations", None)
110
106
  if isinstance(ann, dict):
111
107
  ro = ann.get("readOnlyHint")
@@ -113,12 +109,13 @@ def tool_to_spec(server_id: str, tool):
113
109
  else:
114
110
  ro = getattr(ann, "readOnlyHint", None) if ann else None
115
111
  de = getattr(ann, "destructiveHint", None) if ann else None
112
+ se: SideEffect
116
113
  if ro is True:
117
114
  se, destr = "read", False
118
115
  elif de is True:
119
116
  se, destr = "write", True
120
117
  else:
121
- se, destr = "write", False # fail-closed:无注解按有副作用
118
+ se, destr = "write", False
122
119
  tname = _get(tool, "name", "") or ""
123
120
  return SkillSpec(
124
121
  id=f"mcp__{server_id}__{tname}", name=tname,
@@ -129,7 +126,6 @@ def tool_to_spec(server_id: str, tool):
129
126
  )
130
127
 
131
128
 
132
- # ── 供 SkillAdapter 的客户端代理 ──────────────────────────────
133
129
  class _ClientProxy:
134
130
  def __init__(self, mgr, sid):
135
131
  self._mgr, self._sid = mgr, sid
@@ -140,8 +136,6 @@ class _ClientProxy:
140
136
  raise RuntimeError(f"MCP server {self._sid} 未登记")
141
137
  from foxclaw.core.tracing import span as _span, current_traceparent
142
138
  with _span("mcp.call_tool", server=self._sid, tool=str(tool)):
143
- # 跨进程传播:带上 W3C traceparent,对端(外部 MCP server)若支持即可接上同一条链。
144
- # 这是 Foxclaw 唯一真实的跨进程边界,也是"分布式追踪"在本系统里唯一有意义之处。
145
139
  headers = dict(cfg["headers"] or {})
146
140
  tp = current_traceparent()
147
141
  if tp:
@@ -28,9 +28,6 @@ class McpStore:
28
28
  def save(self, servers: list):
29
29
  with self._lock:
30
30
  try:
31
- # 【P-Muse·第4步】守门人模式下主进程只存元数据(headers 为空),正常不触发。
32
- # 一旦有 headers 非空,说明走的是回退路径(或误传凭证)——明文凭证即将落盘,
33
- # 主动告警,消除"凭证静默落盘、重启后才暴露"的隐患。
34
31
  if any(dict(s.get("headers") or {}) for s in servers):
35
32
  print("[McpStore] ⚠ 检测到 headers 凭证,将明文落盘到 "
36
33
  f"{self._path.name}(当前为回退路径/未启用守门人)。",
@@ -16,25 +16,20 @@ from typing import Literal, Optional
16
16
 
17
17
  Decision = Literal["allow", "ask", "deny"]
18
18
 
19
- # 【P5-1】ask 的结构化原因码。此前 ask + requires_confirmation=True 只有一个语义:
20
- # 「用户确认后就能执行」。但 Stage 1 的「缺参数」不是「需确认」——用户确认一百次
21
- # 也补不上参数,必须先补数据。reason_code 让调用方(前端/编排层/审计)能区分:
22
- MISSING_PARAMS = "MISSING_PARAMS" # 缺必填参数,不可通过确认绕过(先补数据)
23
- DESTRUCTIVE_CONFIRM = "DESTRUCTIVE_CONFIRM" # 不可逆操作,需确认
24
- SIDE_EFFECT_CONFIRM = "SIDE_EFFECT_CONFIRM" # write/send/pay 副作用,需确认
25
- ASK_RULE = "ASK_RULE" # 命中宿主 ask 规则
26
- EXTERNAL_CONFIRM = "EXTERNAL_CONFIRM" # 外部调用,需确认
27
- TAINT_CONFIRM = "TAINT_CONFIRM" # 会话已取回敏感数据,外发/副作用须二次确认
28
- # deny / allow 侧文稿未定义 code,保守留 "":deny 的铁律语义(deny 不可覆盖)已由
29
- # decision + stage 充分表达,不擅自扩大枚举面。
19
+ MISSING_PARAMS = "MISSING_PARAMS"
20
+ DESTRUCTIVE_CONFIRM = "DESTRUCTIVE_CONFIRM"
21
+ SIDE_EFFECT_CONFIRM = "SIDE_EFFECT_CONFIRM"
22
+ ASK_RULE = "ASK_RULE"
23
+ EXTERNAL_CONFIRM = "EXTERNAL_CONFIRM"
24
+ TAINT_CONFIRM = "TAINT_CONFIRM"
30
25
 
31
26
 
32
27
  @dataclass
33
28
  class PermissionRules:
34
29
  """宿主提供的规则集。deny 优先级最高且不可覆盖。"""
35
- deny: list = field(default_factory=list) # skill id / glob,命中即拒绝
36
- ask: list = field(default_factory=list) # 命中需确认
37
- allow: list = field(default_factory=list) # 命中放行
30
+ deny: list = field(default_factory=list)
31
+ ask: list = field(default_factory=list)
32
+ allow: list = field(default_factory=list)
38
33
 
39
34
  @staticmethod
40
35
  def _match(patterns: list, skill_id: str) -> bool:
@@ -54,11 +49,10 @@ class PermissionRules:
54
49
  @dataclass
55
50
  class PermissionDecision:
56
51
  decision: Decision
57
- stage: int # 哪个阶段做出的决定(1-4)
52
+ stage: int
58
53
  reason: str = ""
59
- trust_downgrade: bool = False # 结果是否须按「不可信」处理(MCP/远程)
54
+ trust_downgrade: bool = False
60
55
  requires_confirmation: bool = False
61
- # 【P5-1】ask 的结构化原因码(见模块顶部常量);非 ask 或文稿未定义的情形为 ""
62
56
  reason_code: str = ""
63
57
 
64
58
  def to_dict(self) -> dict:
@@ -85,9 +79,6 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
85
79
  rules = rules or PermissionRules()
86
80
  sid = skill.id
87
81
 
88
- # ---------- Stage 1: validate(输入合法性)----------
89
- # 参数 schema 基本校验:声明的必填参数是否齐全
90
- # 兼容两种 params 格式:{"k": "str"}(简单)或 {"k": {"required": True}}(详细)
91
82
  params = getattr(skill, "params", {}) or {}
92
83
  missing = []
93
84
  for k, spec in params.items():
@@ -95,59 +86,37 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
95
86
  if required and k not in (args or {}):
96
87
  missing.append(k)
97
88
  if missing:
98
- # 校验失败 → 优雅降级为 ask(不是安全拒绝,是数据不全)
99
- # 【P5-1】MISSING_PARAMS:确认无法绕过缺参数,调用方须先补数据
100
89
  return PermissionDecision("ask", 1,
101
90
  reason=f"缺少必填参数: {', '.join(missing)}",
102
91
  requires_confirmation=True, reason_code=MISSING_PARAMS)
103
92
 
104
- # ---------- Stage 2: rule-match(deny > ask > allow 铁律)----------
105
93
  if rules.hits_deny(sid):
106
94
  return PermissionDecision("deny", 2, reason=f"命中 deny 规则(不可覆盖): {sid}")
107
95
  ask_by_rule = rules.hits_ask(sid)
108
96
  allow_by_rule = rules.hits_allow(sid)
109
- # deny 不命中时,ask 优先于 allow
110
97
 
111
- # ---------- Stage 3: capability(能力检查,Fail-closed)----------
112
- side_effect = getattr(skill, "side_effect", "write") # 未声明 → 按有副作用(fail-closed)
98
+ side_effect = getattr(skill, "side_effect", "write")
113
99
  destructive = getattr(skill, "destructive", False)
114
100
  external = getattr(skill, "external", False)
115
- # read_only 字段默认 None → 按 side_effect 推断(read 即只读);显式 True/False 则尊重
116
101
  _ro = getattr(skill, "read_only", None)
117
102
  read_only = (side_effect == "read") if _ro is None else _ro
118
- # 单一真源(P6 修复):信任分级由 SkillSpec.trust_level() 唯一裁定。此前 permission 另存一份
119
- # TRUST_LEVEL dict,与 trust_level() 重复且含死 key(bundled/remote —— kind 永不取此二值);对 3 个
120
- # 合法 SkillKind(local/web_api/mcp)二者 downgrade 行为完全一致,故收敛为单一真源、行为不变。
121
- # duck-typed skill 无 trust_level 方法时 fail-closed 当低信任。
122
- # 【P5-0】trust_level() 抛异常 → 当低信任(fail-closed),不让权限管线崩。
123
- # 第三方 skill 的 trust_level() 若有 bug(如 AttributeError),原实现会让异常直接冒泡、
124
- # evaluate 崩溃,用户看到「调用失败」而非「权限拒绝」——这是 fail-open。改为兜住异常、
125
- # 按最低信任(low)处理:无法判定信任等级时,宁可保守。
126
103
  _tl = getattr(skill, "trust_level", None)
127
104
  try:
128
105
  trust = _tl() if callable(_tl) else "low"
129
106
  except Exception:
130
107
  trust = "low"
131
- downgrade = trust == "low" # 低信任(mcp/远程)结果须按不可信处理
108
+ downgrade = trust == "low"
132
109
 
133
- # readonly 模式:非只读一律拒绝
134
110
  if mode == "readonly" and not read_only:
135
111
  return PermissionDecision("deny", 3,
136
112
  reason="readonly 模式下禁止非只读操作", trust_downgrade=downgrade)
137
113
 
138
- # destructive 永不自动放行(即使有 allow 规则也要确认)
139
114
  if destructive and not confirmed:
140
115
  return PermissionDecision("ask", 3,
141
116
  reason="不可逆(destructive)操作,必须用户确认",
142
117
  trust_downgrade=downgrade, requires_confirmation=True,
143
118
  reason_code=DESTRUCTIVE_CONFIRM)
144
119
 
145
- # Stage 3.5 污点门槛(Muse 式「碰过敏感数据 → 外发动作追加确认」):
146
- # 会话已取回 ≥threshold 的用户数据,再外发/副作用 → 即使工具本身可信也强制确认,
147
- # 防「模型把用户隐私塞进 send/write/external」外泄。置于 trusted 之前:
148
- # 污点是结构性约束,trusted 只信任工具本身,不豁免「携带敏感数据外发」。
149
- # [P-Muse·确认放行] `not confirmed`:此闸防的是**静默**外发,用户显式确认后必须放行,
150
- # 否则「问一次 → 确认 → 再问一次」永远出不去(与上方 destructive 的判定口径一致)。
151
120
  if taint_threshold and taint_level >= taint_threshold and not confirmed:
152
121
  if side_effect in ("write", "send", "pay") or external:
153
122
  return PermissionDecision("ask", 3,
@@ -155,26 +124,22 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
155
124
  trust_downgrade=downgrade, requires_confirmation=True,
156
125
  reason_code=TAINT_CONFIRM)
157
126
 
158
- # trusted 模式:跳过 ask,只保留 deny(上面已查)+ destructive(上面已查)
159
127
  if mode == "trusted":
160
128
  return PermissionDecision("allow", 3,
161
129
  reason="trusted 模式放行", trust_downgrade=downgrade)
162
130
 
163
- # ---------- Stage 4: confirm(确认)----------
164
131
  needs_confirm = (
165
132
  ask_by_rule or external or side_effect in ("write", "send", "pay")
166
133
  )
167
134
  if needs_confirm and not confirmed:
168
135
  why = ("命中 ask 规则" if ask_by_rule else
169
136
  "外部调用" if external else f"{side_effect} 副作用")
170
- # 【P5-1】三个分支各自的结构化原因码
171
137
  code = (ASK_RULE if ask_by_rule else
172
138
  EXTERNAL_CONFIRM if external else SIDE_EFFECT_CONFIRM)
173
139
  return PermissionDecision("ask", 4,
174
140
  reason=f"需确认: {why}", trust_downgrade=downgrade,
175
141
  requires_confirmation=True, reason_code=code)
176
142
 
177
- # 放行:显式 allow 规则,或只读无副作用,或已确认
178
143
  reason = ("命中 allow 规则" if allow_by_rule else
179
144
  "只读无副作用" if read_only else "已确认")
180
145
  return PermissionDecision("allow", 4, reason=reason, trust_downgrade=downgrade)