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
@@ -6,16 +6,17 @@ Foxclaw 行程子系统 — Itinerary Runtime
6
6
 
7
7
  import csv
8
8
  from pathlib import Path
9
+ from typing import cast
9
10
 
10
- from .models import Waypoint, ItineraryVerdict
11
- from .reasoning import ItineraryReasoning
11
+ from .models import ItineraryVerdict, TravelMode, Waypoint
12
+ from .reasoning import MODE_SPEED, ItineraryReasoning
12
13
  from .amap_client import AMapClient
13
14
 
14
15
 
15
16
  class ItineraryRuntime:
16
17
  """行政 Agent 的行程内核。"""
17
18
 
18
- def __init__(self, amap_key: str = None, hmr=None):
19
+ def __init__(self, amap_key: str | None = None, hmr=None):
19
20
  self.waypoints: list = []
20
21
  self.reasoning = ItineraryReasoning()
21
22
  self.amap = AMapClient(amap_key)
@@ -25,13 +26,11 @@ class ItineraryRuntime:
25
26
  def amap_enabled(self) -> bool:
26
27
  return self.amap.enabled
27
28
 
28
- # ── 采集行程点 ────────────────────────────────────────
29
- def add_waypoint(self, name: str, address: str = "", lng: float = None,
30
- lat: float = None, start_time: str = None,
29
+ def add_waypoint(self, name: str, address: str = "", lng: float | None = None,
30
+ lat: float | None = None, start_time: str | None = None,
31
31
  duration_min: int = 0, city: str = "") -> dict:
32
32
  wp = Waypoint(name=name, address=address, lng=lng, lat=lat,
33
33
  start_time=start_time, duration_min=duration_min, city=city)
34
- # 没坐标但有地址 → 尝试高德地理编码
35
34
  if not wp.has_coord and address and self.amap_enabled:
36
35
  geo = self.amap.geocode(address, city)
37
36
  if geo:
@@ -62,7 +61,6 @@ class ItineraryRuntime:
62
61
  count += 1
63
62
  return count
64
63
 
65
- # ── 真实路线(高德优先,否则估算)─────────────────────
66
64
  def real_leg(self, a: Waypoint, b: Waypoint, mode: str = "driving"):
67
65
  """两点路线:高德可用且有坐标时用真实数据,否则直线估算。"""
68
66
  if self.amap_enabled and a.has_coord and b.has_coord:
@@ -70,12 +68,12 @@ class ItineraryRuntime:
70
68
  if r:
71
69
  from .models import RouteLeg
72
70
  return RouteLeg(from_id=a.id, to_id=b.id, from_name=a.name,
73
- to_name=b.name, mode=mode,
71
+ to_name=b.name,
72
+ mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
74
73
  distance_km=r["distance_km"],
75
74
  duration_min=r["duration_min"], source="amap")
76
75
  return self.reasoning.estimate_leg(a, b, mode)
77
76
 
78
- # ── 综合分析(行政 Agent 核心输出)────────────────────
79
77
  def plan(self, mode: str = "driving", optimize: bool = True) -> dict:
80
78
  """完整行程规划:优化顺序 + 冲突检测 + 聚类 + 守门人裁决。"""
81
79
  opt = (self.reasoning.optimize_order(self.waypoints, mode)
@@ -95,7 +93,6 @@ class ItineraryRuntime:
95
93
  "map_data": self._to_amap_mapdata(opt),
96
94
  }
97
95
 
98
- # ── 行程守门人 ────────────────────────────────────────
99
96
  def _gatekeeper(self, conflicts, clusters, dens) -> list:
100
97
  verdicts = []
101
98
  for c in conflicts:
@@ -124,7 +121,6 @@ class ItineraryRuntime:
124
121
  payload={"cluster": cl}))
125
122
  return verdicts
126
123
 
127
- # ── 导出高德可视化格式(MapTaskData)──────────────────
128
124
  def _to_amap_mapdata(self, opt) -> list:
129
125
  """转成高德 MapTaskData(POI + route),可喂给高德生成可视化地图。"""
130
126
  data = []
@@ -25,8 +25,8 @@ class CapabilityRegistry:
25
25
  def __init__(self):
26
26
  self._caps: dict[str, Any] = {}
27
27
  self._lazy: dict[str, Callable[[], Any]] = {}
28
- self._lock = threading.RLock() # 只护 _caps/_lazy 表(快照,持锁时间极短)
29
- self._build_locks: dict[str, threading.Lock] = {} # name -> 构造锁(per-name)
28
+ self._lock = threading.RLock()
29
+ self._build_locks: dict[str, threading.Lock] = {}
30
30
 
31
31
  def register(self, name: str, handle: Any) -> None:
32
32
  """注册一个已构造好的能力实例(覆盖同名)。"""
@@ -38,7 +38,7 @@ class CapabilityRegistry:
38
38
  with self._lock:
39
39
  self._lazy[name] = factory
40
40
 
41
- def get(self, name: str, default: Any = None) -> Any:
41
+ def get(self, name: str, default: Any | None = None) -> Any:
42
42
  """解析能力。已注册直接返回;惰性则首次构造并缓存;都没有返回 default。
43
43
 
44
44
  【F1 修复】慢惰性构造(如 search_runtime 首建要装 HKC+载模型)**不持全局锁**,
@@ -54,12 +54,11 @@ class CapabilityRegistry:
54
54
  blk = self._build_locks.get(name)
55
55
  if blk is None:
56
56
  blk = self._build_locks[name] = threading.Lock()
57
- # 出全局锁,用 per-name 锁串行化"同一能力"的构造
58
57
  with blk:
59
58
  with self._lock:
60
- if name in self._caps: # 双检:构造期间可能已被别的线程建好
59
+ if name in self._caps:
61
60
  return self._caps[name]
62
- inst = factory() # 慢构造:不持全局锁,不阻塞其它能力解析
61
+ inst = factory()
63
62
  with self._lock:
64
63
  self._caps[name] = inst
65
64
  return inst
@@ -22,12 +22,10 @@ from ..core.governance_audit import new_decision_id
22
22
  @dataclass
23
23
  class Verdict:
24
24
  allowed: bool
25
- level: str # allow | warn | deny | confirm
25
+ level: str
26
26
  reason: str = ""
27
- source: str = "" # harness | permission
28
- raw: Any = None # 原始 HarnessResult / PermissionDecision(调用方按需取用)
29
- # 【P3-4】每次裁决的唯一 ID(P1-2 治理审计的关联键)。放最后+默认值:既有位置参数
30
- # 构造(如 Verdict(True,"allow",...,"permission",None))不受影响。
27
+ source: str = ""
28
+ raw: Any = None
31
29
  decision_id: str = field(default="")
32
30
 
33
31
 
@@ -35,9 +33,7 @@ class PolicyGate:
35
33
  def __init__(self, spine=None, action_evaluator: Optional[Callable] = None,
36
34
  auditor=None):
37
35
  self._spine = spine
38
- self._eval = action_evaluator # skills.permission.evaluate,注入(避免 kernel→skills 环)
39
- # 【P1-2】统一治理审计(可空:standalone 无审计)。鸭子类型注入,避免 kernel→core 之外
40
- # 的装配环;core.governance_audit.GovernanceAuditor 是唯一预期实现。
36
+ self._eval = action_evaluator
41
37
  self._auditor = auditor
42
38
 
43
39
  @property
@@ -45,7 +41,6 @@ class PolicyGate:
45
41
  """SkillAdapter 经此取审计器,把执行终态关联到裁决 decision_id(P3-4 跨层关联)。"""
46
42
  return self._auditor
47
43
 
48
- # ── 推理准入(Harness)──────────────────────────────────────
49
44
  def check_reasoning(self, harness, node) -> Verdict:
50
45
  """经统一门做推理漂移准入。底层仍是那一次 harness.check(同一 HarnessResult),
51
46
  Harness 自身已 emit harness_block/warn 事件,门面**不重复 emit**。行为一致。
@@ -64,7 +59,6 @@ class PolicyGate:
64
59
  "dominant_dim": getattr(r, "dominant_dim", "")})
65
60
  return v
66
61
 
67
- # ── 动作准入(skill-guard)──────────────────────────────────
68
62
  def check_action(self, skill, args: dict, rules=None,
69
63
  confirmed: bool = False, mode: str = "strict",
70
64
  taint_level: int = 0, taint_threshold: int = 0) -> Verdict:
@@ -16,16 +16,16 @@ import threading
16
16
  import time
17
17
  import uuid
18
18
  from dataclasses import dataclass, field
19
+ from typing import Optional
19
20
 
20
21
 
21
- # 合法状态与允许的流转(状态机约束:防止非法跳转)
22
22
  _VALID_TRANSITIONS = {
23
23
  "created": {"queued", "failed"},
24
24
  "queued": {"scheduled", "failed"},
25
25
  "scheduled": {"running", "failed"},
26
26
  "running": {"completed", "failed"},
27
- "completed": set(), # 终态
28
- "failed": set(), # 终态
27
+ "completed": set(),
28
+ "failed": set(),
29
29
  }
30
30
  _TERMINAL = {"completed", "failed"}
31
31
 
@@ -33,15 +33,15 @@ _TERMINAL = {"completed", "failed"}
33
33
  @dataclass
34
34
  class ProcessRecord:
35
35
  task: str
36
- agent_id: str = None
36
+ agent_id: Optional[str] = None
37
37
  state: str = "created"
38
38
  process_id: str = field(default_factory=lambda: f"proc_{uuid.uuid4().hex[:8]}")
39
39
  priority: float = 0.0
40
- route_hint: str = None
40
+ route_hint: Optional[str] = None
41
41
  created_at: float = field(default_factory=time.time)
42
42
  updated_at: float = field(default_factory=time.time)
43
43
  state_history: list = field(default_factory=list)
44
- fail_reason: str = None
44
+ fail_reason: Optional[str] = None
45
45
 
46
46
  def to_dict(self) -> dict:
47
47
  return {
@@ -58,21 +58,13 @@ class ProcessManager:
58
58
 
59
59
  def __init__(self, bus=None, history_limit: int = 1000):
60
60
  self._processes: dict[str, ProcessRecord] = {}
61
- self.bus = bus # 可选:状态变更发事件
61
+ self.bus = bus
62
62
  self._history_limit = history_limit
63
- # 【深审优化】活跃(非终态)进程数的 O(1) 增量计数:create 时 +1、进入终态时 -1
64
- # (集中在 _transition)。供 CognitiveState 每次 lifecycle 事件 O(1) 取用,
65
- # 免去原先每事件 stats() 遍历全部进程(O(n),n≤1000)。
66
63
  self._active = 0
67
- # 并行子 Agent 会让多个线程并发 create/mark_*/_evict 同一个 _processes dict,
68
- # 且状态机流转是"读 state → 校验 → 写 state"的非原子序列。RLock 保证:
69
- # · dict 增删与遍历(list/stats/_evict)不撞"dict changed size";
70
- # · 同一进程的并发二次流转不会都通过终态校验后重复 transition。
71
- # 可重入:_emit→bus.publish 的处理器可能回调 pm 的方法(同线程重入安全)。
72
64
  self._lock = threading.RLock()
73
65
 
74
- def create(self, task: str, agent_id: str = None,
75
- schedule: dict = None) -> ProcessRecord:
66
+ def create(self, task: str, agent_id: str | None = None,
67
+ schedule: dict | None = None) -> ProcessRecord:
76
68
  """创建进程(任务进入系统)。携带调度决策。"""
77
69
  rec = ProcessRecord(
78
70
  task=task, agent_id=agent_id,
@@ -81,36 +73,33 @@ class ProcessManager:
81
73
  )
82
74
  with self._lock:
83
75
  rec.state_history.append("created")
84
- # 创建后立即入队 → 已调度(调度决策此时已有)
85
76
  self._transition(rec, "queued")
86
77
  if schedule:
87
78
  self._transition(rec, "scheduled")
88
79
  self._processes[rec.process_id] = rec
89
- self._active += 1 # 新进程为活跃(此刻处于 queued/scheduled 非终态)
80
+ self._active += 1
90
81
  self._evict_if_needed()
91
82
  self._emit(rec)
92
83
  return rec
93
84
 
94
- def mark_running(self, process_id: str) -> ProcessRecord:
85
+ def mark_running(self, process_id: str) -> Optional[ProcessRecord]:
95
86
  with self._lock:
96
87
  rec = self._processes.get(process_id)
97
88
  if rec is None:
98
89
  return None
99
- # 若仍在 queued(create 未带 schedule),先补 scheduled 再 running
100
90
  if rec.state == "queued":
101
91
  self._transition(rec, "scheduled")
102
92
  return self._safe_transition(process_id, "running")
103
93
 
104
- def mark_completed(self, process_id: str) -> ProcessRecord:
94
+ def mark_completed(self, process_id: str) -> Optional[ProcessRecord]:
105
95
  with self._lock:
106
96
  return self._safe_transition(process_id, "completed")
107
97
 
108
- def mark_failed(self, process_id: str, reason: str = "") -> ProcessRecord:
98
+ def mark_failed(self, process_id: str, reason: str = "") -> Optional[ProcessRecord]:
109
99
  with self._lock:
110
100
  rec = self._processes.get(process_id)
111
101
  if rec is None:
112
102
  return None
113
- # 已是终态 → 幂等忽略(重试/并发下的二次标记不算错误,不崩溃)
114
103
  if rec.state in _TERMINAL:
115
104
  return rec
116
105
  self._transition(rec, "failed")
@@ -118,11 +107,11 @@ class ProcessManager:
118
107
  self._emit(rec)
119
108
  return rec
120
109
 
121
- def get(self, process_id: str) -> ProcessRecord:
110
+ def get(self, process_id: str) -> Optional[ProcessRecord]:
122
111
  with self._lock:
123
112
  return self._processes.get(process_id)
124
113
 
125
- def list(self, state: str = None, limit: int = 50) -> list:
114
+ def list(self, state: str | None = None, limit: int = 50) -> list:
126
115
  with self._lock:
127
116
  items = list(self._processes.values())
128
117
  if state:
@@ -138,21 +127,18 @@ class ProcessManager:
138
127
 
139
128
  def stats(self) -> dict:
140
129
  with self._lock:
141
- counts = {}
130
+ counts: dict[str, int] = {}
142
131
  for r in self._processes.values():
143
132
  counts[r.state] = counts.get(r.state, 0) + 1
144
133
  return {"total": len(self._processes), "by_state": counts,
145
134
  "active": self._active}
146
135
 
147
- # ── 内部 ──
148
- def _safe_transition(self, process_id: str, next_state: str) -> ProcessRecord:
136
+ def _safe_transition(self, process_id: str, next_state: str) -> Optional[ProcessRecord]:
149
137
  rec = self._processes.get(process_id)
150
138
  if rec is None:
151
139
  return None
152
- # 已是终态 → 幂等忽略(不崩溃)
153
140
  if rec.state in _TERMINAL:
154
141
  return rec
155
- # 已在目标状态 → 幂等忽略(重复标记不报错)
156
142
  if rec.state == next_state:
157
143
  return rec
158
144
  self._transition(rec, next_state)
@@ -167,15 +153,12 @@ class ProcessManager:
167
153
  新增调用点直接调本方法,活跃计数会被重复扣减(可扣成负数,stats 失真)。
168
154
  这里是双保险。注意行为变更:终态 → 任何状态,从抛 ValueError 变为静默忽略。
169
155
  """
170
- # 已是终态 → 幂等忽略(不再 -1,不再改状态)
171
156
  if rec.state in _TERMINAL:
172
157
  return
173
158
  allowed = _VALID_TRANSITIONS.get(rec.state, set())
174
159
  if next_state not in allowed:
175
160
  raise ValueError(
176
161
  f"非法状态流转: {rec.state} → {next_state}(进程 {rec.process_id})")
177
- # 进入终态 → 活跃计数 -1(每进程恰好 create 时 +1、首次入终态时 -1;
178
- # 上面的终态守卫保证不会重复扣减)。
179
162
  if next_state in _TERMINAL:
180
163
  self._active -= 1
181
164
  rec.state = next_state
@@ -183,8 +166,6 @@ class ProcessManager:
183
166
  rec.state_history.append(next_state)
184
167
 
185
168
  def _emit(self, rec: ProcessRecord):
186
- # 【v2 ① Step3·Lifecycle 统一】emit 唯一生命周期词汇 task.lifecycle.<state>
187
- # (取代旧 process.state_changed;Scheduler 委托本 pm,不再自发另一套词汇)。
188
169
  if self.bus is not None:
189
170
  try:
190
171
  self.bus.publish(f"task.lifecycle.{rec.state}", {
@@ -29,7 +29,7 @@ class _PriorityAdmission:
29
29
  self.max = max_slots
30
30
  self._free = max_slots or 0
31
31
  self._cv = threading.Condition()
32
- self._heap: list = [] # (-priority, seq):优先级降序,同优先级 FIFO
32
+ self._heap: list = []
33
33
  self._seq = 0
34
34
 
35
35
  def acquire(self, priority: int = 0) -> None:
@@ -43,7 +43,7 @@ class _PriorityAdmission:
43
43
  self._cv.wait()
44
44
  heapq.heappop(self._heap)
45
45
  self._free -= 1
46
- if self._free > 0 and self._heap: # 还有空槽+等待者 → 唤醒下一个最高优先者
46
+ if self._free > 0 and self._heap:
47
47
  self._cv.notify_all()
48
48
 
49
49
  def release(self) -> None:
@@ -58,11 +58,8 @@ class Scheduler:
58
58
  def __init__(self, max_concurrency: Optional[int] = None, spine=None,
59
59
  process_manager=None):
60
60
  self.max_concurrency = max_concurrency if (max_concurrency and max_concurrency > 0) else None
61
- # Step5:优先级准入(满载时高优先排队者先获空位;不抢占正在跑的)。无上限时零等待。
62
61
  self._adm = _PriorityAdmission(self.max_concurrency)
63
62
  self._spine = spine
64
- # 【Step3】统一生命周期:有 ProcessManager 则委托它发 task.lifecycle.*(唯一词汇、
65
- # 唯一状态机);无 pm(standalone/测试)则调度器自发 task.lifecycle.* 作回退。
66
63
  self.process_manager = process_manager
67
64
  self._lock = threading.Lock()
68
65
  self._stats = {"submitted": 0, "running": 0, "completed": 0,
@@ -80,7 +77,6 @@ class Scheduler:
80
77
  proc = pm.create(task, agent_id=agent_id, schedule={"priority": priority})
81
78
  else:
82
79
  self._emit("scheduled", agent_id, task, priority)
83
- # 优先级准入:满载则排队等空位(高优先先入);无上限时零等待(behavior-preserving)
84
80
  self._adm.acquire(priority)
85
81
  try:
86
82
  with self._lock:
@@ -90,8 +86,6 @@ class Scheduler:
90
86
  if proc is not None:
91
87
  pm.mark_running(proc.process_id)
92
88
  else:
93
- # 【P5-2】回退路径的词汇与 ProcessManager 对齐:started → running
94
- # (pm 的词汇是 created/queued/scheduled/running/completed/failed)
95
89
  self._emit("running", agent_id, task, priority)
96
90
  result = fn()
97
91
  with self._lock:
@@ -116,7 +110,7 @@ class Scheduler:
116
110
 
117
111
  def stats(self) -> dict:
118
112
  with self._lock:
119
- s = dict(self._stats)
113
+ s: dict[str, int | None] = dict(self._stats)
120
114
  s["max_concurrency"] = self.max_concurrency
121
115
  return s
122
116
 
@@ -23,12 +23,9 @@ import secrets
23
23
  import sys
24
24
  import time
25
25
  from pathlib import Path
26
+ from typing import Any
26
27
 
27
28
 
28
- # ── 凭证保险箱(vault)────────────────────────────────────────
29
- # 真实凭证只进守门人进程,主进程内存里从不出现。两个来源:
30
- # 1. 环境变量前缀 FOXCLAW_GUARDIAN_SECRET_<REF>(REF 统一小写);
31
- # 2. FOXCLAW_GUARDIAN_SECRETS 指向的 JSON 文件 {"ref": "secret", ...}。
32
29
  def load_vault() -> dict:
33
30
  vault: dict = {}
34
31
  prefix = "FOXCLAW_GUARDIAN_SECRET_"
@@ -42,13 +39,10 @@ def load_vault() -> dict:
42
39
  if isinstance(d, dict):
43
40
  vault.update({str(k).lower(): v for k, v in d.items()})
44
41
  except Exception:
45
- pass # 损坏的 secrets 文件不拖垮守门人;宁可少借不错借
42
+ pass
46
43
  return vault
47
44
 
48
45
 
49
- # ── MCP server 配置(守门人持有凭证)──────────────────────────
50
- # 与 vault 对称:MCP 的 headers 里可能含真凭证(Authorization/x-api-key 等)。
51
- # 主进程通过 mcp_register 推送配置;也可用 FOXCLAW_GUARDIAN_MCP_SERVERS 指向 JSON 文件做初始加载。
52
46
  def _mcp_servers_path() -> str:
53
47
  """守门人侧 MCP 配置持久化文件。优先 FOXCLAW_GUARDIAN_MCP_SERVERS 显式指定;
54
48
  否则用 FOXCLAW_GUARDIAN_DATA_DIR 下的 mcp_guardian_servers.json。主进程的 mcp_servers.json 已不含 headers。"""
@@ -79,7 +73,7 @@ def load_mcp_servers() -> dict:
79
73
  "transport": c.get("transport") or "streamable_http",
80
74
  }
81
75
  except Exception:
82
- pass # 损坏配置不拖垮守门人;宁可少配不错配
76
+ pass
83
77
  return servers
84
78
 
85
79
 
@@ -94,18 +88,15 @@ def _persist_mcp_servers(servers: dict) -> None:
94
88
  tmp.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
95
89
  tmp.replace(Path(path))
96
90
  except Exception:
97
- pass # 持久化失败不拖垮守门人;重启后主进程会重新 mcp_register
91
+ pass
98
92
 
99
93
 
100
- # ── 短期可撤销能力令牌 ──────────────────────────────────────
101
- # 彻底取代「resolve 真值借出」:主进程只拿得到一个不透明随机 token,真值永不出守门人进程。
102
- # 令牌 = 一次性 + 过期 + 可撤销,进程内状态表(不依赖 HMAC 签名,因为 token 本身高熵、无法伪造/猜中)。
103
94
  class TokenStore:
104
95
  def __init__(self, default_ttl: float = 60.0):
105
96
  self._default_ttl = default_ttl
106
97
  self._tokens: dict = {}
107
98
 
108
- def mint(self, ref: str, ttl: float = None) -> dict:
99
+ def mint(self, ref: str, ttl: float | None = None) -> dict:
109
100
  """为某 ref 签发能力令牌。返回 {token, expires_in},绝不返回真值。"""
110
101
  ttl = ttl if ttl is not None else self._default_ttl
111
102
  token = secrets.token_urlsafe(32)
@@ -137,7 +128,6 @@ class TokenStore:
137
128
  return True
138
129
 
139
130
 
140
- # ── 跨进程重建(守门人侧)─────────────────────────────────────
141
131
  def _rebuild_skill(sd: dict):
142
132
  """主进程只传 evaluate 需要的字段 + 已算好的 trust。重建为鸭子类型对象。"""
143
133
  from types import SimpleNamespace
@@ -145,7 +135,7 @@ def _rebuild_skill(sd: dict):
145
135
  return SimpleNamespace(
146
136
  id=sd.get("id", ""),
147
137
  kind=sd.get("kind", "local"),
148
- side_effect=sd.get("side_effect", "write"), # 未声明 → 有副作用
138
+ side_effect=sd.get("side_effect", "write"),
149
139
  destructive=bool(sd.get("destructive", False)),
150
140
  external=bool(sd.get("external", False)),
151
141
  read_only=sd.get("read_only", None),
@@ -187,7 +177,7 @@ def _handle_mint(msg: dict, vault: dict, store: TokenStore) -> dict:
187
177
  ttl = float(ttl) if ttl else None
188
178
  out = store.mint(ref, ttl)
189
179
  print(f"[Sentinel] mint {ref}", file=sys.stderr, flush=True)
190
- return out # {"token", "expires_in"} —— 不含真值
180
+ return out
191
181
 
192
182
 
193
183
  def _handle_revoke(msg: dict, store: TokenStore) -> dict:
@@ -212,7 +202,7 @@ def _handle_mcp_register(msg: dict, servers: dict) -> dict:
212
202
  headers = dict(c.get("headers") or {})
213
203
  old = servers.get(sid)
214
204
  if not headers and old:
215
- headers = dict(old.get("headers") or {}) # 主进程不持 headers,不覆盖守门人已有凭证
205
+ headers = dict(old.get("headers") or {})
216
206
  new_servers[sid] = {
217
207
  "name": c.get("name") or sid,
218
208
  "url": c.get("url", ""),
@@ -251,7 +241,7 @@ def _handle_mcp_call(msg: dict, servers: dict) -> dict:
251
241
  if out.get("isError"):
252
242
  return SkillResult(skill_id, False, status="blocked",
253
243
  message=f"MCP 工具返回错误: {out.get('content') or 'unknown'}").to_dict()
254
- out = _mark_untrusted(out) # 外部 MCP 返回=不可信,防提示注入
244
+ out = _mark_untrusted(out)
255
245
  print(f"[Sentinel] mcp_call {server_id}.{tool}", file=sys.stderr, flush=True)
256
246
  return SkillResult(skill_id, True, output=out,
257
247
  message="MCP 调用完成(外部不可信数据)").to_dict()
@@ -329,7 +319,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
329
319
  secret = None
330
320
  used_ref = None
331
321
  if token:
332
- ref = store.redeem(token) # 一次性:兑换即作废
322
+ ref = store.redeem(token)
333
323
  if ref is None:
334
324
  return SkillResult(skill_id, False, status="blocked",
335
325
  message="能力令牌无效/已过期/已使用/已撤销").to_dict()
@@ -345,7 +335,6 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
345
335
  return SkillResult(skill_id, False, status="blocked",
346
336
  message=f"凭证 ref「{secret_ref}」未在守门人 vault 中配置").to_dict()
347
337
 
348
- # SSRF 校验:守门人侧也做(主进程可能被攻破传内网地址,不能只信主进程)
349
338
  block = ssrf_check(endpoint, allow_local=allow_local)
350
339
  if block:
351
340
  return SkillResult(skill_id, False, status="blocked",
@@ -364,7 +353,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
364
353
  return SkillResult(skill_id, False, status="blocked",
365
354
  message="缺少 httpx 依赖,无法发起 Web API 调用").to_dict()
366
355
 
367
- req_kwargs = {"headers": headers, "timeout": timeout, "follow_redirects": False}
356
+ req_kwargs: dict[str, Any] = {"headers": headers, "timeout": timeout, "follow_redirects": False}
368
357
  if loc == "query":
369
358
  req_kwargs["params"] = args
370
359
  else:
@@ -416,6 +405,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
416
405
  mid = msg.get("id")
417
406
  op = msg.get("op")
418
407
  try:
408
+ result: dict[str, object]
419
409
  if op == "ping":
420
410
  result = {"pong": True}
421
411
  elif op == "evaluate":
@@ -438,7 +428,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
438
428
  else:
439
429
  result = {"error": f"unknown op: {op}"}
440
430
  _reply({"id": mid, "ok": True, "result": result})
441
- except Exception as e: # 单条请求失败不影响守门人存活
431
+ except Exception as e:
442
432
  _reply({"id": mid, "ok": False, "error": str(e)})
443
433
 
444
434
 
@@ -22,20 +22,18 @@ class SentinelUnavailable(RuntimeError):
22
22
 
23
23
 
24
24
  def _project_root() -> str:
25
- # foxclaw/kernel/sentinel_client.py → parents[0]=kernel, [1]=foxclaw, [2]=Foxclaw
26
25
  return str(Path(__file__).resolve().parents[2])
27
26
 
28
27
 
29
28
  class SentinelClient:
30
- def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str = None):
29
+ def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str | None = None):
31
30
  self._timeout = timeout
32
31
  self._data_dir = data_dir
33
32
  self._lock = threading.Lock()
34
33
  self._req_id = 0
35
- self._proc = None
34
+ self._proc: subprocess.Popen[str] | None = None
36
35
  self._spawn(cmd)
37
36
 
38
- # ── 进程生命周期 ─────────────────────────────────────────
39
37
  def _spawn(self, cmd) -> None:
40
38
  if cmd is None:
41
39
  cmd = [sys.executable, "-m", "foxclaw.kernel.sentinel"]
@@ -50,7 +48,7 @@ class SentinelClient:
50
48
  except Exception as e:
51
49
  raise RuntimeError(f"Sentinel 启动失败: {e}")
52
50
  try:
53
- self._request("ping", {}) # 握手:启动失败/坏路径在此显式报错,不静默降级
51
+ self._request("ping", {})
54
52
  except Exception as e:
55
53
  self._kill()
56
54
  raise RuntimeError(f"Sentinel 握手失败: {e}")
@@ -73,16 +71,18 @@ class SentinelClient:
73
71
  except Exception:
74
72
  pass
75
73
 
76
- # ── IPC 核心 ─────────────────────────────────────────────
77
74
  def _request(self, op: str, payload: dict):
78
75
  with self._lock:
79
76
  self._req_id += 1
80
77
  mid = self._req_id
81
78
  msg = json.dumps({"id": mid, "op": op, **payload}, ensure_ascii=False)
82
79
  try:
83
- self._proc.stdin.write(msg + "\n")
84
- self._proc.stdin.flush()
85
- line = self._proc.stdout.readline()
80
+ proc = self._proc
81
+ if proc is None or proc.stdin is None or proc.stdout is None:
82
+ raise SentinelUnavailable("守门人进程未启动")
83
+ proc.stdin.write(msg + "\n")
84
+ proc.stdin.flush()
85
+ line = proc.stdout.readline()
86
86
  except Exception as e:
87
87
  raise SentinelUnavailable(f"IPC 通信失败: {e}")
88
88
  if not line:
@@ -95,7 +95,6 @@ class SentinelClient:
95
95
  raise SentinelUnavailable(resp.get("error", "unknown error"))
96
96
  return resp.get("result")
97
97
 
98
- # ── 序列化(只传 evaluate 需要的字段,不含 handler/凭证)────
99
98
  @staticmethod
100
99
  def _serialize_skill(skill) -> dict:
101
100
  _tl = getattr(skill, "trust_level", None)
@@ -106,7 +105,7 @@ class SentinelClient:
106
105
  return {
107
106
  "id": getattr(skill, "id", ""),
108
107
  "kind": getattr(skill, "kind", "local"),
109
- "side_effect": getattr(skill, "side_effect", "write"), # 未声明 → 有副作用
108
+ "side_effect": getattr(skill, "side_effect", "write"),
110
109
  "destructive": bool(getattr(skill, "destructive", False)),
111
110
  "external": bool(getattr(skill, "external", False)),
112
111
  "read_only": getattr(skill, "read_only", None),
@@ -124,7 +123,6 @@ class SentinelClient:
124
123
  "allow": list(getattr(rules, "allow", []) or []),
125
124
  }
126
125
 
127
- # ── 对外接口 ─────────────────────────────────────────────
128
126
  def evaluate(self, skill, args, rules=None, confirmed=False, mode="strict",
129
127
  taint_level=0, taint_threshold=0):
130
128
  """动作裁决(跨进程)。签名对齐 skills.permission.evaluate,返回 PermissionDecision。"""
@@ -141,7 +139,6 @@ class SentinelClient:
141
139
  try:
142
140
  d = self._request("evaluate", payload)
143
141
  except SentinelUnavailable as e:
144
- # fail-closed:守门人不可达 → deny,绝不降级放行
145
142
  return PermissionDecision("deny", 0,
146
143
  reason=f"守门人不可达(fail-closed): {e}",
147
144
  reason_code="SENTINEL_UNAVAILABLE")
@@ -154,9 +151,9 @@ class SentinelClient:
154
151
  reason_code=d.get("reason_code", ""),
155
152
  )
156
153
 
157
- def mint(self, ref: str, ttl: float = None):
154
+ def mint(self, ref: str, ttl: float | None = None):
158
155
  """为 ref 签发一次性能力令牌。返回 token(绝不返回真值);失败返回 None。"""
159
- payload = {"ref": ref}
156
+ payload: dict[str, object] = {"ref": ref}
160
157
  if ttl is not None:
161
158
  payload["ttl"] = ttl
162
159
  try:
@@ -32,7 +32,6 @@ class Event:
32
32
  source: str = ""
33
33
  correlation_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
34
34
 
35
- # ── 与旧 BusMessage 同形,便于 SystemBus 无痛门面化 ──
36
35
  @property
37
36
  def channel(self) -> str:
38
37
  return self.topic
@@ -46,8 +45,8 @@ class EventSpine:
46
45
  """进程内唯一事件脊柱。发布/订阅(前缀匹配) + 投影 + 有界历史。线程安全。"""
47
46
 
48
47
  def __init__(self, history_limit: int = 1000):
49
- self._subs: list[tuple[str, Callable]] = [] # (topic_prefix, handler)
50
- self._projections: list[Callable] = [] # 每条事件都收到
48
+ self._subs: list[tuple[str, Callable]] = []
49
+ self._projections: list[Callable] = []
51
50
  self._history: deque = deque(maxlen=history_limit)
52
51
  self._lock = threading.RLock()
53
52
 
@@ -61,13 +60,15 @@ class EventSpine:
61
60
  with self._lock:
62
61
  self._projections.append(sink)
63
62
 
64
- def publish(self, topic: str, payload: dict = None, source: str = "",
65
- correlation_id: str = None) -> Event:
66
- evt = Event(topic=topic, payload=payload or {}, source=source,
67
- **({"correlation_id": correlation_id} if correlation_id else {}))
63
+ def publish(self, topic: str, payload: dict | None = None, source: str = "",
64
+ correlation_id: str | None = None) -> Event:
65
+ if correlation_id:
66
+ evt = Event(topic=topic, payload=payload or {}, source=source,
67
+ correlation_id=correlation_id)
68
+ else:
69
+ evt = Event(topic=topic, payload=payload or {}, source=source)
68
70
  with self._lock:
69
71
  self._history.append(evt)
70
- # 锁内只做"快照",handler/sink 在锁外执行(避免与 pm/hmr 锁形成环)
71
72
  handlers = [h for pfx, h in self._subs if topic.startswith(pfx)]
72
73
  projections = list(self._projections)
73
74
  for h in handlers:
@@ -90,7 +91,6 @@ class EventSpine:
90
91
 
91
92
  def stats(self) -> dict:
92
93
  with self._lock:
93
- # 【P4-2】补 history_limit(deque.maxlen):只报 history_size 无法判断「是否已满」。
94
94
  return {"subscriptions": len(self._subs),
95
95
  "projections": len(self._projections),
96
96
  "history_size": len(self._history),