foxclaw 0.1.0 → 0.1.2

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