foxclaw 0.1.0 → 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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,443 @@
1
+ """Sentinel —— Foxclaw 的独立守门人进程(Muse「Harness/Sentinel 分离」的本地最小版)。
2
+
3
+ 背景:foxclaw 的动作裁决(skills.permission.evaluate)+ 统一门(kernel.policy_gate)此前都是
4
+ **主 Agent 进程内的软件模块**。好处是零开销、易测;代价是:一旦 Agent 进程被 RCE,裁决逻辑、
5
+ 规则表、以及散落在 os.environ 里的真实凭证会一起暴露 —— 那是「用提示词约束模型」的残留,
6
+ 不是「用系统结构约束 Agent」。
7
+
8
+ 本模块把两件事挪出主进程,放进一个**独立进程**:
9
+ 1. evaluate —— 动作裁决(纯函数,跨进程执行);
10
+ 2. mint/redeem —— 凭证代注入(真实凭证只存在于守门人进程,主进程只拿一次性可撤销令牌,真值永不出进程);
11
+ 3. mcp_register/mcp_call —— MCP 代执行(守门人持有 MCP server 配置与凭证,主进程只发意图)。
12
+
13
+ 跨平台:stdio JSON-lines(Windows 无 unix socket)。父进程 spawn 本进程经管道通信。
14
+ 默认关闭:不设 FOXCLAW_GUARDIAN=1 时,foxclaw 完全走原进程内路径,行为逐字不变。
15
+
16
+ 独立启动(部署到常开机时也可这样常驻): python -m foxclaw.kernel.sentinel
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import secrets
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+
29
+ def load_vault() -> dict:
30
+ vault: dict = {}
31
+ prefix = "FOXCLAW_GUARDIAN_SECRET_"
32
+ for k, v in os.environ.items():
33
+ if k.startswith(prefix) and v:
34
+ vault[k[len(prefix):].lower()] = v
35
+ path = os.environ.get("FOXCLAW_GUARDIAN_SECRETS", "").strip()
36
+ if path and Path(path).exists():
37
+ try:
38
+ d = json.loads(Path(path).read_text(encoding="utf-8"))
39
+ if isinstance(d, dict):
40
+ vault.update({str(k).lower(): v for k, v in d.items()})
41
+ except Exception:
42
+ pass
43
+ return vault
44
+
45
+
46
+ def _mcp_servers_path() -> str:
47
+ """守门人侧 MCP 配置持久化文件。优先 FOXCLAW_GUARDIAN_MCP_SERVERS 显式指定;
48
+ 否则用 FOXCLAW_GUARDIAN_DATA_DIR 下的 mcp_guardian_servers.json。主进程的 mcp_servers.json 已不含 headers。"""
49
+ p = os.environ.get("FOXCLAW_GUARDIAN_MCP_SERVERS", "").strip()
50
+ if p:
51
+ return p
52
+ d = os.environ.get("FOXCLAW_GUARDIAN_DATA_DIR", "").strip()
53
+ if d:
54
+ return str(Path(d) / "mcp_guardian_servers.json")
55
+ return ""
56
+
57
+
58
+ def load_mcp_servers() -> dict:
59
+ servers: dict = {}
60
+ path = _mcp_servers_path()
61
+ if not path or not Path(path).exists():
62
+ return servers
63
+ try:
64
+ d = json.loads(Path(path).read_text(encoding="utf-8"))
65
+ for c in d.get("servers", []) or []:
66
+ sid = str(c.get("id", "")).strip()
67
+ if not sid:
68
+ continue
69
+ servers[sid] = {
70
+ "name": c.get("name") or sid,
71
+ "url": c.get("url", ""),
72
+ "headers": dict(c.get("headers") or {}),
73
+ "transport": c.get("transport") or "streamable_http",
74
+ }
75
+ except Exception:
76
+ pass
77
+ return servers
78
+
79
+
80
+ def _persist_mcp_servers(servers: dict) -> None:
81
+ path = _mcp_servers_path()
82
+ if not path:
83
+ return
84
+ try:
85
+ out = {"servers": [dict(v) for v in servers.values()]}
86
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
87
+ tmp = Path(path).with_suffix(".tmp")
88
+ tmp.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
89
+ tmp.replace(Path(path))
90
+ except Exception:
91
+ pass
92
+
93
+
94
+ class TokenStore:
95
+ def __init__(self, default_ttl: float = 60.0):
96
+ self._default_ttl = default_ttl
97
+ self._tokens: dict = {}
98
+
99
+ def mint(self, ref: str, ttl: float | None = None) -> dict:
100
+ """为某 ref 签发能力令牌。返回 {token, expires_in},绝不返回真值。"""
101
+ ttl = ttl if ttl is not None else self._default_ttl
102
+ token = secrets.token_urlsafe(32)
103
+ self._tokens[token] = {
104
+ "ref": ref,
105
+ "expires_at": time.monotonic() + ttl,
106
+ "used": False,
107
+ "revoked": False,
108
+ }
109
+ return {"token": token, "expires_in": ttl}
110
+
111
+ def redeem(self, token: str):
112
+ """一次性兑换:返回绑定的 ref 并立即作废。无效/过期/已用/已撤销 → None。"""
113
+ rec = self._tokens.get(token or "")
114
+ if rec is None or rec["revoked"] or rec["used"]:
115
+ return None
116
+ if time.monotonic() > rec["expires_at"]:
117
+ self._tokens.pop(token, None)
118
+ return None
119
+ rec["used"] = True
120
+ return rec["ref"]
121
+
122
+ def revoke(self, token: str) -> bool:
123
+ """主动撤销。返回是否真的撤销了一个活跃令牌。"""
124
+ rec = self._tokens.get(token or "")
125
+ if rec is None or rec["used"] or rec["revoked"]:
126
+ return False
127
+ rec["revoked"] = True
128
+ return True
129
+
130
+
131
+ def _rebuild_skill(sd: dict):
132
+ """主进程只传 evaluate 需要的字段 + 已算好的 trust。重建为鸭子类型对象。"""
133
+ from types import SimpleNamespace
134
+ trust = sd.get("trust", "low")
135
+ return SimpleNamespace(
136
+ id=sd.get("id", ""),
137
+ kind=sd.get("kind", "local"),
138
+ side_effect=sd.get("side_effect", "write"),
139
+ destructive=bool(sd.get("destructive", False)),
140
+ external=bool(sd.get("external", False)),
141
+ read_only=sd.get("read_only", None),
142
+ params=sd.get("params", {}) or {},
143
+ trust_level=lambda trust=trust: trust,
144
+ )
145
+
146
+
147
+ def _rebuild_rules(rd: dict):
148
+ from foxclaw.skills.permission import PermissionRules
149
+ return PermissionRules(
150
+ deny=list(rd.get("deny") or []),
151
+ ask=list(rd.get("ask") or []),
152
+ allow=list(rd.get("allow") or []),
153
+ )
154
+
155
+
156
+ def _handle_evaluate(msg: dict) -> dict:
157
+ from foxclaw.skills.permission import evaluate
158
+ skill = _rebuild_skill(msg.get("skill") or {})
159
+ rules = _rebuild_rules(msg.get("rules") or {})
160
+ d = evaluate(
161
+ skill, msg.get("args") or {}, rules=rules,
162
+ confirmed=bool(msg.get("confirmed")),
163
+ mode=msg.get("mode", "strict"),
164
+ taint_level=int(msg.get("taint_level", 0)),
165
+ taint_threshold=int(msg.get("taint_threshold", 0)),
166
+ )
167
+ return d.to_dict()
168
+
169
+
170
+ def _handle_mint(msg: dict, vault: dict, store: TokenStore) -> dict:
171
+ ref = str(msg.get("ref", "")).lower()
172
+ if not ref:
173
+ return {"error": "missing ref"}
174
+ if ref not in vault:
175
+ return {"error": f"unknown secret ref: {ref}"}
176
+ ttl = msg.get("ttl")
177
+ ttl = float(ttl) if ttl else None
178
+ out = store.mint(ref, ttl)
179
+ print(f"[Sentinel] mint {ref}", file=sys.stderr, flush=True)
180
+ return out
181
+
182
+
183
+ def _handle_revoke(msg: dict, store: TokenStore) -> dict:
184
+ token = str(msg.get("token", "") or "")
185
+ if not token:
186
+ return {"error": "missing token"}
187
+ ok = store.revoke(token)
188
+ print(f"[Sentinel] revoke {'ok' if ok else 'miss'}", file=sys.stderr, flush=True)
189
+ return {"revoked": ok}
190
+
191
+
192
+ def _handle_mcp_register(msg: dict, servers: dict) -> dict:
193
+ """主进程推送 MCP server 配置给守门人,全量同步。headers 非空则覆盖、为空则保留旧值:
194
+ 第4步起主进程不长期持有 headers(日常只推元数据),仅在 add_server 那一次临时带 headers。
195
+ 凭证只在守门人进程内,主进程之后只发意图(server_id + tool + args)。"""
196
+ items = msg.get("servers") or []
197
+ new_servers: dict = {}
198
+ for c in items:
199
+ sid = str(c.get("id", "")).strip()
200
+ if not sid:
201
+ continue
202
+ headers = dict(c.get("headers") or {})
203
+ old = servers.get(sid)
204
+ if not headers and old:
205
+ headers = dict(old.get("headers") or {})
206
+ new_servers[sid] = {
207
+ "name": c.get("name") or sid,
208
+ "url": c.get("url", ""),
209
+ "headers": headers,
210
+ "transport": c.get("transport") or "streamable_http",
211
+ }
212
+ servers.clear()
213
+ servers.update(new_servers)
214
+ _persist_mcp_servers(servers)
215
+ print(f"[Sentinel] mcp_register {len(servers)} servers", file=sys.stderr, flush=True)
216
+ return {"count": len(servers)}
217
+
218
+
219
+ def _handle_mcp_call(msg: dict, servers: dict) -> dict:
220
+ """mcp 代执行:守门人进程持有 MCP server 配置(含凭证),在进程内调用 MCP,返回 SkillResult.to_dict()。
221
+ 主进程只传意图(server_id + tool + args),拿不到凭证、不直连外部。"""
222
+ from foxclaw.skills import SkillResult, _mark_untrusted
223
+
224
+ skill_id = msg.get("skill_id", "mcp")
225
+ server_id = str(msg.get("server_id", "") or "")
226
+ tool = str(msg.get("tool", "") or "")
227
+ args = dict(msg.get("args") or {})
228
+
229
+ if not server_id or not tool:
230
+ return SkillResult(skill_id, False, status="not_connected",
231
+ message="未配置 MCP server/tool").to_dict()
232
+ cfg = servers.get(server_id)
233
+ if cfg is None:
234
+ return SkillResult(skill_id, False, status="not_connected",
235
+ message=f"MCP server「{server_id}」未在守门人注册").to_dict()
236
+
237
+ try:
238
+ from foxclaw.skills.mcp_client import _get_bridge, _amcp_call_tool
239
+ out = _get_bridge().run(_amcp_call_tool(
240
+ cfg["url"], dict(cfg.get("headers") or {}), cfg["transport"], tool, args))
241
+ if out.get("isError"):
242
+ return SkillResult(skill_id, False, status="blocked",
243
+ message=f"MCP 工具返回错误: {out.get('content') or 'unknown'}").to_dict()
244
+ out = _mark_untrusted(out)
245
+ print(f"[Sentinel] mcp_call {server_id}.{tool}", file=sys.stderr, flush=True)
246
+ return SkillResult(skill_id, True, output=out,
247
+ message="MCP 调用完成(外部不可信数据)").to_dict()
248
+ except Exception as e:
249
+ return SkillResult(skill_id, False, status="blocked",
250
+ message=f"MCP 调用失败: {e}").to_dict()
251
+
252
+
253
+ def _serialize_tool(tool) -> dict:
254
+ """把 mcp.types.Tool 序列化为纯 dict(跨进程 JSON 可传)。annotations 决定 side_effect/destructive。"""
255
+ ann = getattr(tool, "annotations", None)
256
+ return {
257
+ "name": getattr(tool, "name", "") or "",
258
+ "description": getattr(tool, "description", "") or "",
259
+ "inputSchema": getattr(tool, "inputSchema", {}) or {},
260
+ "annotations": {
261
+ "readOnlyHint": getattr(ann, "readOnlyHint", None) if ann else None,
262
+ "destructiveHint": getattr(ann, "destructiveHint", None) if ann else None,
263
+ },
264
+ }
265
+
266
+
267
+ def _handle_mcp_list_tools(msg: dict, servers: dict) -> dict:
268
+ """发现阶段代执行:守门人用自己持有的 headers 连外部 MCP 拉工具列表,序列化为 dict 列表返回。
269
+ 主进程不碰 headers、不直连外部;工具注解(side_effect/destructive)也由守门人侧真实返回。"""
270
+ server_id = str(msg.get("server_id", "") or "")
271
+ cfg = servers.get(server_id)
272
+ if cfg is None:
273
+ return {"error": f"MCP server「{server_id}」未在守门人注册"}
274
+ try:
275
+ from foxclaw.skills.mcp_client import _get_bridge, _amcp_list_tools
276
+ tools = _get_bridge().run(_amcp_list_tools(
277
+ cfg["url"], dict(cfg.get("headers") or {}), cfg["transport"]))
278
+ out = [_serialize_tool(t) for t in tools]
279
+ print(f"[Sentinel] mcp_list_tools {server_id} -> {len(out)} tools", file=sys.stderr, flush=True)
280
+ return {"tools": out}
281
+ except Exception as e:
282
+ return {"error": f"list_tools 失败: {e}"}
283
+
284
+
285
+ def _inject_secret(headers: dict, args: dict, auth_scheme: str, secret: str) -> None:
286
+ """按 auth_scheme 把真凭证注入请求(只在守门人进程内,真值不出进程)。
287
+ bearer / header:<name> / query:<name>;空或未知 → 不注入(调用方未声明注入方式)。"""
288
+ scheme = (auth_scheme or "").strip()
289
+ if scheme == "bearer":
290
+ headers["Authorization"] = f"Bearer {secret}"
291
+ elif scheme.startswith("header:"):
292
+ headers[scheme[len("header:"):]] = secret
293
+ elif scheme.startswith("query:"):
294
+ args[scheme[len("query:"):]] = secret
295
+
296
+
297
+ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
298
+ """web_api 代执行:守门人进程注入真凭证后发 HTTP,返回 SkillResult.to_dict()。
299
+ 凭证来源优先级:一次性能力令牌(token,先 redeem) > 直接 ref(兼容第 1 步)。
300
+ 真值只在守门人进程内从 vault 取,永不出进程。"""
301
+ from foxclaw.skills import SkillResult
302
+ from foxclaw.skills import ssrf_check
303
+
304
+ skill_id = msg.get("skill_id", "web_api")
305
+ endpoint = msg.get("endpoint", "")
306
+ method = (msg.get("method") or "GET").upper()
307
+ token = str(msg.get("token", "") or "")
308
+ secret_ref = str(msg.get("secret_ref", "")).lower()
309
+ auth_scheme = msg.get("auth_scheme", "bearer") or "bearer"
310
+ args = dict(msg.get("args") or {})
311
+ loc = msg.get("param_location", "auto")
312
+ timeout = float(msg.get("timeout", 10.0))
313
+ allow_local = bool(msg.get("allow_local", False))
314
+
315
+ if not endpoint:
316
+ return SkillResult(skill_id, False, status="not_connected",
317
+ message="未配置 endpoint").to_dict()
318
+
319
+ secret = None
320
+ used_ref = None
321
+ if token:
322
+ ref = store.redeem(token)
323
+ if ref is None:
324
+ return SkillResult(skill_id, False, status="blocked",
325
+ message="能力令牌无效/已过期/已使用/已撤销").to_dict()
326
+ secret = vault.get(ref)
327
+ used_ref = ref
328
+ if secret is None:
329
+ return SkillResult(skill_id, False, status="blocked",
330
+ message=f"令牌绑定的凭证 ref「{ref}」已不在 vault 中").to_dict()
331
+ elif secret_ref:
332
+ secret = vault.get(secret_ref)
333
+ used_ref = secret_ref
334
+ if secret is None:
335
+ return SkillResult(skill_id, False, status="blocked",
336
+ message=f"凭证 ref「{secret_ref}」未在守门人 vault 中配置").to_dict()
337
+
338
+ block = ssrf_check(endpoint, allow_local=allow_local)
339
+ if block:
340
+ return SkillResult(skill_id, False, status="blocked",
341
+ message=f"端点被安全策略拒绝: {block}").to_dict()
342
+
343
+ headers = {"User-Agent": "Foxclaw/1.0"}
344
+ if secret is not None:
345
+ _inject_secret(headers, args, auth_scheme, secret)
346
+
347
+ if loc == "auto":
348
+ loc = "query" if method in ("GET", "DELETE") else "json"
349
+
350
+ try:
351
+ import httpx
352
+ except ImportError:
353
+ return SkillResult(skill_id, False, status="blocked",
354
+ message="缺少 httpx 依赖,无法发起 Web API 调用").to_dict()
355
+
356
+ req_kwargs: dict[str, Any] = {"headers": headers, "timeout": timeout, "follow_redirects": False}
357
+ if loc == "query":
358
+ req_kwargs["params"] = args
359
+ else:
360
+ req_kwargs["json"] = args
361
+
362
+ try:
363
+ with httpx.Client() as client:
364
+ resp = client.request(method, endpoint, **req_kwargs)
365
+ except httpx.TimeoutException:
366
+ return SkillResult(skill_id, False, status="timeout",
367
+ message=f"Web API 调用超时(>{timeout}s)").to_dict()
368
+ except httpx.RequestError as e:
369
+ return SkillResult(skill_id, False, status="blocked",
370
+ message=f"Web API 请求失败: {e}").to_dict()
371
+
372
+ if 300 <= resp.status_code < 400:
373
+ return SkillResult(skill_id, False, status="blocked",
374
+ message=f"Web API 返回重定向 HTTP {resp.status_code}(已阻止跟随)").to_dict()
375
+ if resp.status_code >= 400:
376
+ return SkillResult(skill_id, False, status="blocked",
377
+ message=f"Web API 返回 HTTP {resp.status_code}",
378
+ output={"status_code": resp.status_code, "body": resp.text[:500]}).to_dict()
379
+ try:
380
+ data = resp.json()
381
+ except Exception:
382
+ data = {"text": resp.text[:2000]}
383
+ print(f"[Sentinel] execute_web {method} {endpoint} ref={used_ref or '-'}", file=sys.stderr, flush=True)
384
+ return SkillResult(skill_id, True, status="done",
385
+ output={"status_code": resp.status_code, "data": data},
386
+ message=f"Web API 调用成功(HTTP {resp.status_code})").to_dict()
387
+
388
+
389
+ def _reply(resp: dict) -> None:
390
+ sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
391
+ sys.stdout.flush()
392
+
393
+
394
+ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
395
+ """JSON-lines 循环:stdin 读一行请求 → stdout 回一行响应。父进程退出(stdin 关闭)时自然结束。"""
396
+ for line in sys.stdin:
397
+ line = line.strip()
398
+ if not line:
399
+ continue
400
+ try:
401
+ msg = json.loads(line)
402
+ except Exception:
403
+ _reply({"id": None, "ok": False, "error": "bad json"})
404
+ continue
405
+ mid = msg.get("id")
406
+ op = msg.get("op")
407
+ try:
408
+ result: dict[str, object]
409
+ if op == "ping":
410
+ result = {"pong": True}
411
+ elif op == "evaluate":
412
+ result = _handle_evaluate(msg)
413
+ elif op == "mint":
414
+ result = _handle_mint(msg, vault, store)
415
+ elif op == "revoke":
416
+ result = _handle_revoke(msg, store)
417
+ elif op == "mcp_register":
418
+ result = _handle_mcp_register(msg, servers)
419
+ elif op == "mcp_call":
420
+ result = _handle_mcp_call(msg, servers)
421
+ elif op == "mcp_list_tools":
422
+ result = _handle_mcp_list_tools(msg, servers)
423
+ elif op == "execute_web":
424
+ result = _handle_execute_web(msg, vault, store)
425
+ elif op == "shutdown":
426
+ _reply({"id": mid, "ok": True, "result": {"bye": True}})
427
+ return
428
+ else:
429
+ result = {"error": f"unknown op: {op}"}
430
+ _reply({"id": mid, "ok": True, "result": result})
431
+ except Exception as e:
432
+ _reply({"id": mid, "ok": False, "error": str(e)})
433
+
434
+
435
+ def main(argv=None) -> None:
436
+ vault = load_vault()
437
+ store = TokenStore()
438
+ servers = load_mcp_servers()
439
+ serve_stdio(vault, store, servers)
440
+
441
+
442
+ if __name__ == "__main__":
443
+ main()
@@ -0,0 +1,243 @@
1
+ """SentinelClient —— 主进程侧与守门人进程通信的 IPC 客户端。
2
+
3
+ 用 stdio JSON-lines 与 foxclaw.kernel.sentinel 子进程通信。线程安全(串行锁),超时 fail-closed。
4
+ evaluate 的签名与 skills.permission.evaluate 对齐,返回重建的 PermissionDecision(而非原始 dict),
5
+ 因此可直接注入 kernel.policy_gate 的 action_evaluator,PolicyGate / SkillAdapter 零改动。
6
+
7
+ fail-closed 语义:守门人进程不可达(启动失败/超时/崩溃)时,evaluate 返回 deny —— 宁可拦错
8
+ 不漏放,绝不静默降级回「进程内放行」。mint 失败返回 None,execute_web 失败返回 blocked。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import threading
17
+ from pathlib import Path
18
+
19
+
20
+ class SentinelUnavailable(RuntimeError):
21
+ pass
22
+
23
+
24
+ def _project_root() -> str:
25
+ return str(Path(__file__).resolve().parents[2])
26
+
27
+
28
+ class SentinelClient:
29
+ def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str | None = None):
30
+ self._timeout = timeout
31
+ self._data_dir = data_dir
32
+ self._lock = threading.Lock()
33
+ self._req_id = 0
34
+ self._proc: subprocess.Popen[str] | None = None
35
+ self._spawn(cmd)
36
+
37
+ def _spawn(self, cmd) -> None:
38
+ if cmd is None:
39
+ cmd = [sys.executable, "-m", "foxclaw.kernel.sentinel"]
40
+ try:
41
+ env = dict(os.environ)
42
+ if self._data_dir:
43
+ env["FOXCLAW_GUARDIAN_DATA_DIR"] = str(self._data_dir)
44
+ self._proc = subprocess.Popen(
45
+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
46
+ stderr=subprocess.DEVNULL, text=True, encoding="utf-8",
47
+ errors="replace", bufsize=1, cwd=_project_root(), env=env)
48
+ except Exception as e:
49
+ raise RuntimeError(f"Sentinel 启动失败: {e}")
50
+ try:
51
+ self._request("ping", {})
52
+ except Exception as e:
53
+ self._kill()
54
+ raise RuntimeError(f"Sentinel 握手失败: {e}")
55
+
56
+ def _kill(self) -> None:
57
+ if self._proc is not None:
58
+ try:
59
+ self._proc.kill()
60
+ except Exception:
61
+ pass
62
+
63
+ def close(self) -> None:
64
+ if self._proc is not None and self._proc.poll() is None:
65
+ try:
66
+ self._request("shutdown", {})
67
+ except Exception:
68
+ pass
69
+ try:
70
+ self._proc.terminate()
71
+ except Exception:
72
+ pass
73
+
74
+ def _request(self, op: str, payload: dict):
75
+ with self._lock:
76
+ self._req_id += 1
77
+ mid = self._req_id
78
+ msg = json.dumps({"id": mid, "op": op, **payload}, ensure_ascii=False)
79
+ try:
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
+ except Exception as e:
87
+ raise SentinelUnavailable(f"IPC 通信失败: {e}")
88
+ if not line:
89
+ raise SentinelUnavailable("守门人进程已退出")
90
+ try:
91
+ resp = json.loads(line)
92
+ except Exception:
93
+ raise SentinelUnavailable("守门人返回非法 JSON")
94
+ if not resp.get("ok"):
95
+ raise SentinelUnavailable(resp.get("error", "unknown error"))
96
+ return resp.get("result")
97
+
98
+ @staticmethod
99
+ def _serialize_skill(skill) -> dict:
100
+ _tl = getattr(skill, "trust_level", None)
101
+ try:
102
+ trust = _tl() if callable(_tl) else "low"
103
+ except Exception:
104
+ trust = "low"
105
+ return {
106
+ "id": getattr(skill, "id", ""),
107
+ "kind": getattr(skill, "kind", "local"),
108
+ "side_effect": getattr(skill, "side_effect", "write"),
109
+ "destructive": bool(getattr(skill, "destructive", False)),
110
+ "external": bool(getattr(skill, "external", False)),
111
+ "read_only": getattr(skill, "read_only", None),
112
+ "params": getattr(skill, "params", {}) or {},
113
+ "trust": trust,
114
+ }
115
+
116
+ @staticmethod
117
+ def _serialize_rules(rules) -> dict:
118
+ if rules is None:
119
+ return {}
120
+ return {
121
+ "deny": list(getattr(rules, "deny", []) or []),
122
+ "ask": list(getattr(rules, "ask", []) or []),
123
+ "allow": list(getattr(rules, "allow", []) or []),
124
+ }
125
+
126
+ def evaluate(self, skill, args, rules=None, confirmed=False, mode="strict",
127
+ taint_level=0, taint_threshold=0):
128
+ """动作裁决(跨进程)。签名对齐 skills.permission.evaluate,返回 PermissionDecision。"""
129
+ from foxclaw.skills.permission import PermissionDecision
130
+ payload = {
131
+ "skill": self._serialize_skill(skill),
132
+ "args": args or {},
133
+ "rules": self._serialize_rules(rules),
134
+ "confirmed": bool(confirmed),
135
+ "mode": mode,
136
+ "taint_level": taint_level,
137
+ "taint_threshold": taint_threshold,
138
+ }
139
+ try:
140
+ d = self._request("evaluate", payload)
141
+ except SentinelUnavailable as e:
142
+ return PermissionDecision("deny", 0,
143
+ reason=f"守门人不可达(fail-closed): {e}",
144
+ reason_code="SENTINEL_UNAVAILABLE")
145
+ return PermissionDecision(
146
+ decision=d.get("decision", "deny"),
147
+ stage=int(d.get("stage", 0)),
148
+ reason=d.get("reason", ""),
149
+ trust_downgrade=bool(d.get("trust_downgrade", False)),
150
+ requires_confirmation=bool(d.get("requires_confirmation", False)),
151
+ reason_code=d.get("reason_code", ""),
152
+ )
153
+
154
+ def mint(self, ref: str, ttl: float | None = None):
155
+ """为 ref 签发一次性能力令牌。返回 token(绝不返回真值);失败返回 None。"""
156
+ payload: dict[str, object] = {"ref": ref}
157
+ if ttl is not None:
158
+ payload["ttl"] = ttl
159
+ try:
160
+ return self._request("mint", payload).get("token")
161
+ except SentinelUnavailable:
162
+ return None
163
+
164
+ def revoke(self, token: str) -> bool:
165
+ """主动撤销令牌。返回是否真的撤销了一个活跃令牌;失败返回 False。"""
166
+ try:
167
+ return bool(self._request("revoke", {"token": token}).get("revoked"))
168
+ except SentinelUnavailable:
169
+ return False
170
+
171
+ def mcp_register(self, servers: list) -> bool:
172
+ """把 MCP server 配置(含 headers 凭证)推送给守门人,覆盖式更新。失败返回 False。"""
173
+ try:
174
+ d = self._request("mcp_register", {"servers": list(servers or [])})
175
+ return isinstance(d, dict) and int(d.get("count", 0)) >= 0
176
+ except SentinelUnavailable:
177
+ return False
178
+
179
+ def mcp_call(self, skill, args) -> dict:
180
+ """mcp 代执行:主进程只传意图(server_id + tool + args),守门人持有凭证并调用。
181
+ 返回 SkillResult.to_dict()。fail-closed:守门人不可达 → blocked(绝不落到主进程直连外部)。"""
182
+ from foxclaw.skills import SkillResult
183
+ skill_id = getattr(skill, "id", "")
184
+ payload = {
185
+ "skill_id": skill_id,
186
+ "server_id": getattr(skill, "mcp_server", "") or "",
187
+ "tool": getattr(skill, "mcp_tool", "") or "",
188
+ "args": args or {},
189
+ }
190
+ try:
191
+ d = self._request("mcp_call", payload)
192
+ except SentinelUnavailable as e:
193
+ return SkillResult(skill_id, False, status="blocked",
194
+ message=f"守门人不可达(fail-closed): {e}").to_dict()
195
+ if not isinstance(d, dict):
196
+ return SkillResult(skill_id, False, status="blocked",
197
+ message="守门人返回异常").to_dict()
198
+ return d
199
+
200
+ def mcp_list_tools(self, server_id: str):
201
+ """发现阶段代执行:守门人用自己持有的 headers 连外部 MCP 拉工具,返回序列化后的
202
+ dict 列表(每项含 name/description/inputSchema/annotations)。失败抛 SentinelUnavailable(带具体错误)。"""
203
+ d = self._request("mcp_list_tools", {"server_id": server_id})
204
+ if not isinstance(d, dict) or "tools" not in d:
205
+ err = d.get("error") if isinstance(d, dict) else "list_tools 失败"
206
+ raise SentinelUnavailable(f"守门人 list_tools 失败: {err}")
207
+ return list(d["tools"])
208
+
209
+ def execute_web(self, skill, args) -> dict:
210
+ """web_api 代执行:先 mint 一次性令牌,守门人 redeem 后注入真凭证发请求。
211
+ 返回 SkillResult.to_dict()(同状态语义),供 SkillAdapter._invoke_web_via_sentinel 重建。
212
+ fail-closed:守门人不可达/令牌签发失败 → blocked(绝不落到主进程直发,否则凭证/SSRF 双失守)。"""
213
+ from foxclaw.skills import SkillResult
214
+ skill_id = getattr(skill, "id", "")
215
+ secret_ref = getattr(skill, "secret_ref", "") or ""
216
+
217
+ token = None
218
+ if secret_ref:
219
+ token = self.mint(secret_ref)
220
+ if not token:
221
+ return SkillResult(skill_id, False, status="blocked",
222
+ message="签发凭证能力令牌失败(fail-closed)").to_dict()
223
+
224
+ payload = {
225
+ "skill_id": skill_id,
226
+ "endpoint": getattr(skill, "endpoint", ""),
227
+ "method": getattr(skill, "http_method", "GET"),
228
+ "token": token or "",
229
+ "auth_scheme": getattr(skill, "auth_scheme", "bearer") or "bearer",
230
+ "param_location": getattr(skill, "param_location", "auto"),
231
+ "timeout": float(getattr(skill, "timeout", 10.0)),
232
+ "allow_local": bool(getattr(skill, "allow_local", False)),
233
+ "args": args or {},
234
+ }
235
+ try:
236
+ d = self._request("execute_web", payload)
237
+ except SentinelUnavailable as e:
238
+ return SkillResult(skill_id, False, status="blocked",
239
+ message=f"守门人不可达(fail-closed): {e}").to_dict()
240
+ if not isinstance(d, dict):
241
+ return SkillResult(skill_id, False, status="blocked",
242
+ message="守门人返回异常").to_dict()
243
+ return d