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,252 @@
1
+ """
2
+ 横切机制:断路器 + 生命周期钩子 —— AHS FRAMEWORK.md 的代码落地。
3
+
4
+ CircuitBreaker 每个 skill 独立计数,连续失败 N 次 → OPEN → 停止重试 → 降级
5
+ HookManager pre_skill/post_skill/on_error/session_start/session_end +
6
+ agent_start(别名 pre_reasoning,可注入 additionalContext)/
7
+ agent_end(别名 task_complete,只读)——认知管线 run_task 触发(E2)
8
+ 超时受限、容错、可审计;hook 是顾问不是控制器
9
+ (例外:pre_skill 显式返回 deny 可阻断)
10
+
11
+ 对应 docs/internal/HARNESS_STANDARD.md 第二期(稳健性)。
12
+ """
13
+ from __future__ import annotations
14
+ import time
15
+ import fnmatch
16
+ import threading
17
+ from collections import deque
18
+ from dataclasses import dataclass
19
+ from typing import Callable, Optional, Literal
20
+
21
+ from .hooks_spec import HookEvent
22
+
23
+
24
+ CircuitState = Literal["closed", "open", "half_open"]
25
+
26
+
27
+ @dataclass
28
+ class _BreakerEntry:
29
+ failures: int = 0
30
+ state: CircuitState = "closed"
31
+ opened_at: float = 0.0
32
+ probe_in_flight: bool = False
33
+ probe_started_at: float = 0.0
34
+ probe_successes: int = 0
35
+
36
+
37
+ class CircuitBreaker:
38
+ """每个 skill 独立的断路器。标准三态机:closed / open / half_open。
39
+
40
+ - closed:正常放行。连续失败 ≥ threshold → 转 open
41
+ - open:停止调用。冷却 cooldown 秒后 → 转 half_open
42
+ - half_open:只放行 ONE 个探针请求(其余仍拒绝);
43
+ 探针成功累计达 recovery_threshold → 转 closed(恢复);
44
+ 任一探针失败 → 转回 open,重新计冷却
45
+ """
46
+
47
+ def __init__(self, threshold: int = 3, cooldown: float = 30.0,
48
+ recovery_threshold: int = 1, probe_timeout: float = 60.0):
49
+ self.threshold = threshold
50
+ self.cooldown = cooldown
51
+ self.recovery_threshold = max(1, recovery_threshold)
52
+ self.probe_timeout = probe_timeout
53
+ self._entries: dict[str, _BreakerEntry] = {}
54
+ self._lock = threading.RLock()
55
+
56
+ def _get(self, sid: str) -> _BreakerEntry:
57
+ if sid not in self._entries:
58
+ self._entries[sid] = _BreakerEntry()
59
+ return self._entries[sid]
60
+
61
+ def allow(self, sid: str) -> bool:
62
+ """该 skill 现在是否允许调用。
63
+
64
+ closed → True;open(冷却中)→ False;
65
+ open(冷却满)→ 转 half_open 并放行这一个探针;
66
+ half_open → 仅当没有探针在飞时放行一个,否则 False。
67
+ """
68
+ with self._lock:
69
+ e = self._get(sid)
70
+ if e.state == "closed":
71
+ return True
72
+ if e.state == "open":
73
+ if time.time() - e.opened_at >= self.cooldown:
74
+ e.state = "half_open"
75
+ e.probe_in_flight = True
76
+ e.probe_started_at = time.time()
77
+ e.probe_successes = 0
78
+ return True
79
+ return False
80
+ if e.state == "half_open":
81
+ if e.probe_in_flight:
82
+ if time.time() - e.probe_started_at >= self.probe_timeout:
83
+ e.probe_in_flight = False
84
+ else:
85
+ return False
86
+ e.probe_in_flight = True
87
+ e.probe_started_at = time.time()
88
+ return True
89
+ return False
90
+
91
+ def record_success(self, sid: str):
92
+ with self._lock:
93
+ e = self._get(sid)
94
+ if e.state == "half_open":
95
+ e.probe_in_flight = False
96
+ e.probe_successes += 1
97
+ if e.probe_successes >= self.recovery_threshold:
98
+ e.state = "closed"
99
+ e.failures = 0
100
+ e.probe_successes = 0
101
+ else:
102
+ e.failures = 0
103
+ e.state = "closed"
104
+
105
+ def record_failure(self, sid: str):
106
+ with self._lock:
107
+ e = self._get(sid)
108
+ if e.state == "half_open":
109
+ e.state = "open"
110
+ e.opened_at = time.time()
111
+ e.probe_in_flight = False
112
+ e.probe_successes = 0
113
+ return
114
+ e.failures += 1
115
+ if e.failures >= self.threshold:
116
+ e.state = "open"
117
+ e.opened_at = time.time()
118
+
119
+ def status(self, sid: str) -> dict:
120
+ with self._lock:
121
+ e = self._get(sid)
122
+ state = e.state
123
+ if e.state == "open" and time.time() - e.opened_at >= self.cooldown:
124
+ state = "half_open"
125
+ return {"state": state, "failures": e.failures,
126
+ "threshold": self.threshold,
127
+ "probe_successes": e.probe_successes,
128
+ "recovery_threshold": self.recovery_threshold}
129
+
130
+ def is_open(self, sid: str) -> bool:
131
+ """只读查询:该 skill 当前是否处于「阻止调用」状态。
132
+
133
+ 【P4-0】原实现写的是 `state == "open" and not self.allow(sid)` —— 但 allow() 在
134
+ 「open 且冷却已满」时会**副作用性地**转入 half_open 并占掉唯一的探针名额。于是
135
+ 「只想看一眼断路器开着没」的监控代码会意外消耗掉恢复探测机会,把真正该放行的探针
136
+ 挡在外面。这里改成纯读,不做任何状态迁移。
137
+
138
+ 语义:open 且冷却未满 → 阻止;open 且冷却已满 → 视作「下次调用会被放行」,不计为打开;
139
+ half_open 仅当探针在飞且未超时时计入。
140
+ """
141
+ with self._lock:
142
+ e = self._get(sid)
143
+ if e.state == "open":
144
+ return (time.time() - e.opened_at) < self.cooldown
145
+ if e.state == "half_open":
146
+ return e.probe_in_flight and (time.time() - e.probe_started_at) < self.probe_timeout
147
+ return False
148
+
149
+
150
+ @dataclass
151
+ class Hook:
152
+ event: HookEvent
153
+ matcher: str
154
+ handler: Callable
155
+ name: str = ""
156
+
157
+ def matches(self, skill_id: str) -> bool:
158
+ return fnmatch.fnmatch(skill_id or "", self.matcher)
159
+
160
+
161
+ @dataclass
162
+ class HookResult:
163
+ blocked: bool = False
164
+ reason: str = ""
165
+ modified_args: Optional[dict] = None
166
+ additional_context: Optional[str] = None
167
+
168
+
169
+ class HookManager:
170
+ """生命周期钩子管理。钩子超时受限、容错、可审计。
171
+
172
+ 规则(对齐 FRAMEWORK.md):
173
+ - 超时受限:单个 hook 超过 timeout 视为出错(不阻塞链路)
174
+ - 容错:hook 失败不中断 skill 链(顾问不是控制器)
175
+ - 例外:pre_skill 显式返回 {block:True} 可阻断(这是它被授予的权力)
176
+ - 可审计:每次 hook 执行记录到 audit_log
177
+ """
178
+
179
+ def __init__(self, timeout: float = 10.0, audit_limit: int = 2000):
180
+ self.timeout = timeout
181
+ self._hooks: list[Hook] = []
182
+ self.audit_log: deque = deque(maxlen=max(100, audit_limit))
183
+
184
+ def register(self, event: HookEvent, handler: Callable,
185
+ matcher: str = "*", name: str = ""):
186
+ from .hooks_spec import normalize_hook, is_valid_hook
187
+ norm = normalize_hook(event)
188
+ if not is_valid_hook(norm):
189
+ self.audit_log.append({
190
+ "event": event, "hook": name or getattr(handler, "__name__", "?"),
191
+ "matcher": matcher, "duration_ms": 0, "timed_out": False,
192
+ "error": f"未知钩子点「{event}」,该钩子永不触发(请检查拼写)",
193
+ })
194
+ self._hooks.append(Hook(norm, matcher, handler, name or handler.__name__))
195
+
196
+ def _run_one(self, hook: Hook, ctx: dict) -> dict:
197
+ """执行单个 hook,计时 + 容错。返回 hook 的返回值(dict)或 {}。"""
198
+ t0 = time.time()
199
+ outcome, err = {}, None
200
+ try:
201
+ ret = hook.handler(ctx)
202
+ if isinstance(ret, dict):
203
+ outcome = ret
204
+ except Exception as e:
205
+ err = str(e)
206
+ dt = time.time() - t0
207
+ timed_out = dt > self.timeout
208
+ self.audit_log.append({
209
+ "event": hook.event, "hook": hook.name, "matcher": hook.matcher,
210
+ "duration_ms": round(dt * 1000, 1), "timed_out": timed_out,
211
+ "error": err,
212
+ })
213
+ return {} if err else outcome
214
+
215
+ def fire(self, event: HookEvent, skill_id: str = "", ctx: dict | None = None) -> HookResult:
216
+ """触发某事件的所有匹配 hook。仅 before_tool_call(原 pre_skill)的 block 生效;
217
+ CONTEXT_HOOKS(pre_reasoning/agent_start)返回的 additional_context 会被收集拼接。
218
+
219
+ 【P4-3】⚠ ctx 是**浅拷贝**(dict(...)),不是深拷贝。hook 不得原地修改 ctx 里的嵌套
220
+ dict(如 ctx["args"]["x"])——那会连带改掉调用方的原对象。要改参数请用返回值
221
+ `{"modified_args": {...}}` 整块替换(仅 before_tool_call 生效)。此处刻意不做 deepcopy,
222
+ 以免每次工具调用都付深拷贝开销;约束靠本注释 + 钩子约定维持。
223
+ """
224
+ from .hooks_spec import (normalize_hook, BLOCKING_HOOKS, TOOL_SCOPED_HOOKS,
225
+ CONTEXT_HOOKS)
226
+ event = normalize_hook(event)
227
+ ctx = dict(ctx or {}, event=event, skill_id=skill_id)
228
+ result = HookResult()
229
+ ctx_parts = []
230
+ for hook in self._hooks:
231
+ if hook.event != event:
232
+ continue
233
+ if event in TOOL_SCOPED_HOOKS and not hook.matches(skill_id):
234
+ continue
235
+ out = self._run_one(hook, ctx)
236
+ if event in BLOCKING_HOOKS:
237
+ if out.get("block"):
238
+ result.blocked = True
239
+ result.reason = out.get("reason", f"被 {event} 钩子阻断")
240
+ return result
241
+ if out.get("modified_args") is not None:
242
+ result.modified_args = out["modified_args"]
243
+ if event in CONTEXT_HOOKS:
244
+ inj = out.get("additional_context") or out.get("additionalContext")
245
+ if inj:
246
+ ctx_parts.append(str(inj))
247
+ if ctx_parts:
248
+ result.additional_context = "\n".join(ctx_parts)
249
+ return result
250
+
251
+ def recent_audit(self, n: int = 20) -> list:
252
+ return list(self.audit_log)[-n:]
@@ -0,0 +1,175 @@
1
+ """SKILL.md 指令技能:注入 Agent 提示词的 markdown 指令(非可调用工具)。无代码执行。"""
2
+ from __future__ import annotations
3
+ import re
4
+ import threading
5
+ from dataclasses import dataclass, field, asdict
6
+ import yaml
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class MarkdownSkill:
12
+ id: str
13
+ name: str
14
+ description: str
15
+ triggers: list = field(default_factory=list)
16
+ body: str = ""
17
+ enabled: bool = True
18
+ source: str = "user"
19
+ version: str = ""
20
+ builtin: bool = False
21
+
22
+ def to_dict(self) -> dict:
23
+ d = asdict(self); d["trust"] = "low"; return d
24
+
25
+ def to_meta(self) -> dict:
26
+ d = self.to_dict(); d.pop("body", None); return d
27
+
28
+
29
+ def _slug(s: str) -> str:
30
+ return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_") or "skill"
31
+
32
+
33
+ def parse_skill_md(text: str, id: str | None = None) -> MarkdownSkill:
34
+ """解析 SKILL.md(--- yaml frontmatter --- + 正文)。缺 name/description → ValueError。"""
35
+ text = (text or "").lstrip("").strip()
36
+ m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
37
+ if not m:
38
+ raise ValueError("缺少 --- frontmatter ---")
39
+ try:
40
+ fm = yaml.safe_load(m.group(1)) or {}
41
+ except Exception as e:
42
+ raise ValueError(f"frontmatter 非法 YAML: {e}")
43
+ if not isinstance(fm, dict):
44
+ raise ValueError("frontmatter 需为键值对")
45
+ name = str(fm.get("name") or "").strip()
46
+ desc = str(fm.get("description") or "").strip()
47
+ if not name or not desc:
48
+ raise ValueError("SKILL.md 必须含 name 和 description")
49
+ trig = fm.get("triggers") or []
50
+ if isinstance(trig, str):
51
+ trig = [t.strip() for t in trig.split(",") if t.strip()]
52
+ trig = [str(t) for t in trig]
53
+ builtin = bool(fm.get("builtin", False))
54
+ return MarkdownSkill(id=id or _slug(name), name=name, description=desc,
55
+ triggers=trig, body=m.group(2).strip(),
56
+ source="builtin" if builtin else "user",
57
+ version=str(fm.get("version") or "").strip(),
58
+ builtin=builtin)
59
+
60
+
61
+ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
62
+ """加载随包分发的指令技能:`<root>/<name>/SKILL.md` 中 **frontmatter 标了 builtin: true** 的。
63
+
64
+ 为什么只认 builtin 标记:仓库 `skills/` 下还有 `execution-contract` 之类的**开发者文档**,
65
+ 全量注入会把每轮 prompt 撑肿。显式标记 = 只把"真给模型执行的指令"注入。
66
+ 用户已安装的同 id 技能优先(不覆盖),内置只是缺省值。
67
+ """
68
+ from pathlib import Path
69
+ loaded: list[Any] = []
70
+ try:
71
+ base = Path(root)
72
+ if not base.is_dir():
73
+ return loaded
74
+ for p in sorted(base.glob("*/SKILL.md")):
75
+ try:
76
+ s = parse_skill_md(p.read_text(encoding="utf-8", errors="replace"))
77
+ except Exception:
78
+ continue
79
+ if not s.builtin or reg.get(s.id) is not None:
80
+ continue
81
+ reg.register(s)
82
+ loaded.append(s.id)
83
+ except Exception:
84
+ pass
85
+ return loaded
86
+
87
+
88
+ class MarkdownSkillRegistry:
89
+ def __init__(self):
90
+ self._skills = {}
91
+ self._lock = threading.RLock()
92
+ self.policy = None
93
+
94
+ def register(self, s: MarkdownSkill):
95
+ with self._lock:
96
+ self._skills[s.id] = s
97
+
98
+ def unregister(self, sid: str) -> bool:
99
+ with self._lock:
100
+ return self._skills.pop(sid, None) is not None
101
+
102
+ def get(self, sid: str):
103
+ with self._lock:
104
+ return self._skills.get(sid)
105
+
106
+ def all(self) -> list:
107
+ with self._lock:
108
+ return list(self._skills.values())
109
+
110
+ def catalog(self) -> list:
111
+ return [s.to_meta() for s in self.all()]
112
+
113
+ def match(self, task: str) -> list:
114
+ """有 triggers → 按 triggers(子串);无 triggers(原味 OpenClaw)→ 按 name+description 描述相关。"""
115
+ t_raw = task or ""
116
+ t_low = t_raw.lower()
117
+ out = []
118
+ pol = self.policy
119
+ for s in self.all():
120
+ if not s.enabled:
121
+ continue
122
+ if pol is not None and pol.is_blocked(s.id):
123
+ continue
124
+ if s.triggers:
125
+ if any(k.lower() in t_low for k in s.triggers):
126
+ out.append(s)
127
+ elif _desc_relevant(s, t_low, t_raw):
128
+ out.append(s)
129
+ return out
130
+
131
+
132
+ _ASCII_WORD = re.compile(r"[a-z0-9]{3,}")
133
+ _CJK_RUN = re.compile(r"[一-鿿]{2,}")
134
+
135
+
136
+ def _desc_relevant(skill, t_low: str, t_raw: str) -> bool:
137
+ """无 triggers 时:技能 name+description 与任务是否关键词重叠。
138
+ ASCII 词按词边界(避免 knowledge⊃now 式误命中);中文按 2-gram 子串。"""
139
+ blob = f"{skill.name} {skill.description}"
140
+ for w in set(_ASCII_WORD.findall(blob.lower())):
141
+ if re.search(rf"\b{re.escape(w)}\b", t_low):
142
+ return True
143
+ for run in _CJK_RUN.findall(blob):
144
+ for i in range(len(run) - 1):
145
+ if run[i:i + 2] in t_raw:
146
+ return True
147
+ return False
148
+
149
+
150
+ def extract_skill_package(zip_bytes: bytes, max_uncompressed: int = 2_000_000):
151
+ """从 OpenClaw 包(zip)按名读取 SKILL.md(+可选 _meta.json)。
152
+ 仅按名 read,不 extractall → 无 zip-slip;累计解压超上限 → 防 zip bomb。
153
+ 返回 (skill_md_text, meta_dict)。找不到 SKILL.md → ValueError。"""
154
+ import io
155
+ import json as _json
156
+ import zipfile
157
+ try:
158
+ zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
159
+ except Exception as e:
160
+ raise ValueError(f"不是合法 zip: {e}")
161
+ total = sum(i.file_size for i in zf.infolist())
162
+ if total > max_uncompressed:
163
+ raise ValueError(f"解压体积超上限({total} > {max_uncompressed})")
164
+ skill_name = next((n for n in zf.namelist() if n.lower().endswith("skill.md")), None)
165
+ if not skill_name:
166
+ raise ValueError("包内未找到 SKILL.md")
167
+ skill_md = zf.read(skill_name).decode("utf-8", "replace")
168
+ meta: dict[str, Any] = {}
169
+ meta_name = next((n for n in zf.namelist() if n.lower().endswith("_meta.json")), None)
170
+ if meta_name:
171
+ try:
172
+ meta = _json.loads(zf.read(meta_name).decode("utf-8", "replace")) or {}
173
+ except Exception:
174
+ meta = {}
175
+ return skill_md, meta
@@ -0,0 +1,39 @@
1
+ """SKILL.md 指令技能持久化。<data_dir>/markdown_skills.json。"""
2
+ from __future__ import annotations
3
+ import json
4
+ import threading
5
+ from pathlib import Path
6
+ from dataclasses import fields as _fields
7
+
8
+ from .markdown_skill import MarkdownSkill, MarkdownSkillRegistry
9
+
10
+
11
+ class MarkdownStore:
12
+ def __init__(self, data_dir):
13
+ self._path = Path(data_dir) / "markdown_skills.json"
14
+ self._lock = threading.Lock()
15
+
16
+ def load(self, reg: MarkdownSkillRegistry):
17
+ if not self._path.exists():
18
+ return
19
+ try:
20
+ data = json.loads(self._path.read_text(encoding="utf-8"))
21
+ except Exception:
22
+ return
23
+ allowed = {f.name for f in _fields(MarkdownSkill)}
24
+ for d in data.get("skills", []):
25
+ try:
26
+ reg.register(MarkdownSkill(**{k: v for k, v in d.items() if k in allowed}))
27
+ except Exception:
28
+ continue
29
+
30
+ def save(self, reg: MarkdownSkillRegistry):
31
+ with self._lock:
32
+ payload = {"skills": [s.to_dict() for s in reg.all()]}
33
+ try:
34
+ self._path.parent.mkdir(parents=True, exist_ok=True)
35
+ tmp = self._path.with_suffix(".tmp")
36
+ tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
37
+ tmp.replace(self._path)
38
+ except Exception:
39
+ pass
@@ -0,0 +1,184 @@
1
+ """MCP 连接器(HTTP Streamable/SSE 远程)。mcp SDK 是 async,本模块用后台事件循环线程
2
+ 把它桥成同步,供 SkillAdapter(sync,跨请求/后台线程)调用。按需连接:每次开→用→关。
3
+ 仅远程传输,无本地代码执行。"""
4
+ from __future__ import annotations
5
+ import asyncio
6
+ import threading
7
+ from contextlib import asynccontextmanager
8
+
9
+ from mcp import ClientSession
10
+ from mcp.client.streamable_http import streamablehttp_client
11
+ from mcp.client.sse import sse_client
12
+
13
+
14
+ class _Bridge:
15
+ def __init__(self):
16
+ self._loop = asyncio.new_event_loop()
17
+ t = threading.Thread(target=self._run, daemon=True, name="mcp-loop")
18
+ t.start()
19
+
20
+ def _run(self):
21
+ asyncio.set_event_loop(self._loop)
22
+ self._loop.run_forever()
23
+
24
+ def run(self, coro, timeout: float = 35):
25
+ fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
26
+ try:
27
+ return fut.result(timeout=timeout)
28
+ except BaseException:
29
+ fut.cancel()
30
+ raise
31
+
32
+
33
+ _bridge = None
34
+ _bridge_lock = threading.Lock()
35
+
36
+
37
+ def _get_bridge() -> _Bridge:
38
+ global _bridge
39
+ with _bridge_lock:
40
+ if _bridge is None:
41
+ _bridge = _Bridge()
42
+ return _bridge
43
+
44
+
45
+ async def _ssrf_request_hook(request):
46
+ """httpx 请求钩子:对每个即将发出的请求(初始 + 重定向 follow-up)校验 host。
47
+ 堵 redirect-SSRF + DNS 重绑定 + 配置漂移(恶意 server 把你导去内网/云元数据)。"""
48
+ from .review import _is_internal_host
49
+ host = (request.url.host or "").lower()
50
+ if await asyncio.to_thread(_is_internal_host, host):
51
+ raise RuntimeError(f"MCP 连接被拒:目标解析到内网/本地/元数据地址({host})")
52
+
53
+
54
+ def _safe_httpx_factory(headers=None, timeout=None, auth=None):
55
+ import httpx
56
+ return httpx.AsyncClient(
57
+ headers=headers,
58
+ timeout=timeout if timeout is not None else httpx.Timeout(30.0),
59
+ auth=auth,
60
+ follow_redirects=True,
61
+ event_hooks={"request": [_ssrf_request_hook]},
62
+ )
63
+
64
+
65
+ @asynccontextmanager
66
+ async def _connect(url, headers, transport):
67
+ if transport == "sse":
68
+ async with sse_client(url, headers=headers, httpx_client_factory=_safe_httpx_factory) as (read, write):
69
+ async with ClientSession(read, write) as s:
70
+ await s.initialize()
71
+ yield s
72
+ else:
73
+ async with streamablehttp_client(url, headers=headers, httpx_client_factory=_safe_httpx_factory) as (read, write, _):
74
+ async with ClientSession(read, write) as s:
75
+ await s.initialize()
76
+ yield s
77
+
78
+
79
+ async def _amcp_list_tools(url, headers, transport):
80
+ async with _connect(url, headers, transport) as s:
81
+ return list((await s.list_tools()).tools)
82
+
83
+
84
+ async def _amcp_call_tool(url, headers, transport, name, args):
85
+ async with _connect(url, headers, transport) as s:
86
+ r = await s.call_tool(name, args or {})
87
+ text = "".join(getattr(c, "text", "") for c in (r.content or []))
88
+ return {"isError": bool(getattr(r, "isError", False)), "content": text,
89
+ "structured": getattr(r, "structuredContent", None)}
90
+
91
+
92
+ def _norm(cfg: dict) -> dict:
93
+ return {"id": cfg["id"], "name": cfg.get("name") or cfg["id"], "url": cfg["url"],
94
+ "headers": dict(cfg.get("headers") or {}),
95
+ "transport": cfg.get("transport") or "streamable_http"}
96
+
97
+
98
+ def _get(tool, key, default=None):
99
+ """兼容 dict(守门人序列化后) 与 mcp.types.Tool 对象(直连回退路径)。"""
100
+ return tool.get(key, default) if isinstance(tool, dict) else getattr(tool, key, default)
101
+
102
+
103
+ def tool_to_spec(server_id: str, tool):
104
+ from . import SideEffect, SkillSpec
105
+ ann = _get(tool, "annotations", None)
106
+ if isinstance(ann, dict):
107
+ ro = ann.get("readOnlyHint")
108
+ de = ann.get("destructiveHint")
109
+ else:
110
+ ro = getattr(ann, "readOnlyHint", None) if ann else None
111
+ de = getattr(ann, "destructiveHint", None) if ann else None
112
+ se: SideEffect
113
+ if ro is True:
114
+ se, destr = "read", False
115
+ elif de is True:
116
+ se, destr = "write", True
117
+ else:
118
+ se, destr = "write", False
119
+ tname = _get(tool, "name", "") or ""
120
+ return SkillSpec(
121
+ id=f"mcp__{server_id}__{tname}", name=tname,
122
+ description=_get(tool, "description", "") or "", kind="mcp",
123
+ mcp_server=server_id, mcp_tool=tname, side_effect=se, destructive=destr,
124
+ external=True, params=_get(tool, "inputSchema", {}) or {},
125
+ triggers=[tname], source="user",
126
+ )
127
+
128
+
129
+ class _ClientProxy:
130
+ def __init__(self, mgr, sid):
131
+ self._mgr, self._sid = mgr, sid
132
+
133
+ def call_tool(self, tool, args):
134
+ cfg = self._mgr._servers.get(self._sid)
135
+ if not cfg:
136
+ raise RuntimeError(f"MCP server {self._sid} 未登记")
137
+ from foxclaw.core.tracing import span as _span, current_traceparent
138
+ with _span("mcp.call_tool", server=self._sid, tool=str(tool)):
139
+ headers = dict(cfg["headers"] or {})
140
+ tp = current_traceparent()
141
+ if tp:
142
+ headers["traceparent"] = tp
143
+ out = _get_bridge().run(_amcp_call_tool(cfg["url"], headers, cfg["transport"], tool, args or {}))
144
+ if out.get("isError"):
145
+ raise RuntimeError(out.get("content") or "MCP 工具返回错误")
146
+ return out
147
+
148
+
149
+ class McpManager:
150
+ """服务器配置 + 连接。实现 .get(id) 供 SkillAdapter.mcp_clients 用。"""
151
+ def __init__(self):
152
+ self._servers: dict = {}
153
+ self._lock = threading.RLock()
154
+
155
+ def restore(self, cfg: dict):
156
+ with self._lock:
157
+ self._servers[cfg["id"]] = _norm(cfg)
158
+
159
+ def add_server(self, cfg: dict):
160
+ c = _norm(cfg)
161
+ tools = _get_bridge().run(_amcp_list_tools(c["url"], c["headers"], c["transport"]))
162
+ with self._lock:
163
+ self._servers[c["id"]] = c
164
+ return tools
165
+
166
+ def list_tools(self, sid: str):
167
+ with self._lock:
168
+ c = self._servers.get(sid)
169
+ if not c:
170
+ raise KeyError(sid)
171
+ return _get_bridge().run(_amcp_list_tools(c["url"], c["headers"], c["transport"]))
172
+
173
+ def remove_server(self, sid: str) -> bool:
174
+ with self._lock:
175
+ return self._servers.pop(sid, None) is not None
176
+
177
+ def servers(self) -> list:
178
+ with self._lock:
179
+ return [dict(c) for c in self._servers.values()]
180
+
181
+ def get(self, sid: str):
182
+ with self._lock:
183
+ has = sid in self._servers
184
+ return _ClientProxy(self, sid) if has else None
@@ -0,0 +1,40 @@
1
+ """MCP server 配置持久化。文件 <data_dir>/mcp_servers.json。
2
+
3
+ 【P-Muse·第4步·安全边界】守门人模式(FOXCLAW_GUARDIAN=1)下,主进程只把
4
+ 非敏感元数据(id/name/url/transport)写进这里,headers 凭证只在守门人进程。
5
+ 回退路径(FOXCLAW_GUARDIAN=0,进程内直连)没有守门人保护,headers 会连同配置
6
+ 一起落盘——save 检测到 headers 非空时会打印告警,避免"静默落盘凭证"。
7
+ """
8
+ from __future__ import annotations
9
+ import json
10
+ import sys
11
+ import threading
12
+ from pathlib import Path
13
+
14
+
15
+ class McpStore:
16
+ def __init__(self, data_dir):
17
+ self._path = Path(data_dir) / "mcp_servers.json"
18
+ self._lock = threading.Lock()
19
+
20
+ def load(self) -> list:
21
+ if not self._path.exists():
22
+ return []
23
+ try:
24
+ return json.loads(self._path.read_text(encoding="utf-8")).get("servers", [])
25
+ except Exception:
26
+ return []
27
+
28
+ def save(self, servers: list):
29
+ with self._lock:
30
+ try:
31
+ if any(dict(s.get("headers") or {}) for s in servers):
32
+ print("[McpStore] ⚠ 检测到 headers 凭证,将明文落盘到 "
33
+ f"{self._path.name}(当前为回退路径/未启用守门人)。",
34
+ file=sys.stderr, flush=True)
35
+ self._path.parent.mkdir(parents=True, exist_ok=True)
36
+ tmp = self._path.with_suffix(".tmp")
37
+ tmp.write_text(json.dumps({"servers": servers}, ensure_ascii=False, indent=1), encoding="utf-8")
38
+ tmp.replace(self._path)
39
+ except Exception:
40
+ pass