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,73 @@
1
+ """
2
+ Foxclaw — 标准生命周期钩子点 (Lifecycle Hooks Specification)
3
+
4
+ 借鉴 OpenClaw 的标准钩子命名体系,把原本零散的钩子点规整成清晰的生命周期。
5
+ 钩子分两类:
6
+ · 会话级:贯穿一次 Agent 任务(session_start → agent_start → ... → agent_end → session_end)
7
+ · 工具级:每次 skill 调用(before_tool_call → after_tool_call / on_error / on_loop)
8
+
9
+ 阻断权限:只有 before_tool_call(原 pre_skill)被授予阻断权——它是守门人。
10
+ 其余钩子是观察者(顾问不是控制器),失败不影响主链路。
11
+
12
+ 向后兼容:旧名(pre_skill/post_skill)仍可用,通过 ALIAS 映射到标准名。
13
+ """
14
+ from __future__ import annotations
15
+
16
+
17
+ # ── 标准钩子点(生命周期顺序)──────────────────────────────
18
+ class Hooks:
19
+ """标准钩子点常量。用 Hooks.BEFORE_TOOL_CALL 而非裸字符串,避免拼写错误。"""
20
+
21
+ # 会话级
22
+ SESSION_START = "session_start" # 一次任务开始
23
+ AGENT_START = "agent_start" # Agent 开始处理(分诊后)
24
+ AGENT_END = "agent_end" # Agent 处理完成
25
+ SESSION_END = "session_end" # 一次任务结束
26
+
27
+ # 工具/skill 级
28
+ BEFORE_TOOL_CALL = "before_tool_call" # 调用前(可阻断/可改参数)
29
+ AFTER_TOOL_CALL = "after_tool_call" # 调用成功后
30
+ ON_ERROR = "on_error" # 调用失败
31
+ ON_LOOP = "on_loop" # 检测到死循环
32
+
33
+ # 消息级
34
+ BEFORE_MESSAGE_WRITE = "before_message_write" # 写入/发送消息前(可改写)
35
+
36
+
37
+ # 所有标准钩子点(供校验)
38
+ ALL_HOOKS = frozenset({
39
+ Hooks.SESSION_START, Hooks.AGENT_START, Hooks.AGENT_END, Hooks.SESSION_END,
40
+ Hooks.BEFORE_TOOL_CALL, Hooks.AFTER_TOOL_CALL, Hooks.ON_ERROR, Hooks.ON_LOOP,
41
+ Hooks.BEFORE_MESSAGE_WRITE,
42
+ })
43
+
44
+ # 拥有阻断权的钩子(只有这些返回 {block:True} 才生效)
45
+ BLOCKING_HOOKS = frozenset({Hooks.BEFORE_TOOL_CALL})
46
+
47
+ # 可注入 additionalContext 的钩子(E2:非阻断,但其返回的 additional_context 会被并入推理上下文)。
48
+ # agent_start = 一次 Agent 任务的推理开始点,对应范式 pre_reasoning —— 推理前注入上下文的落点。
49
+ CONTEXT_HOOKS = frozenset({Hooks.AGENT_START})
50
+
51
+ # 工具级钩子(按 skill_id 做 matcher 过滤)
52
+ TOOL_SCOPED_HOOKS = frozenset({
53
+ Hooks.BEFORE_TOOL_CALL, Hooks.AFTER_TOOL_CALL, Hooks.ON_ERROR, Hooks.ON_LOOP,
54
+ })
55
+
56
+ # 向后兼容 + 语义别名 → 标准名
57
+ ALIAS = {
58
+ "pre_skill": Hooks.BEFORE_TOOL_CALL,
59
+ "post_skill": Hooks.AFTER_TOOL_CALL,
60
+ # E2:推理层/会话层语义名(对齐范式 pre_reasoning / Stop),规整到已有的会话级钩子点——
61
+ # 顺带**复活**此前定义却从未被 fire 的 agent_start / agent_end 孤儿钩子。
62
+ "pre_reasoning": Hooks.AGENT_START, # 一次 Agent 任务推理开始前(可注入 additionalContext)
63
+ "task_complete": Hooks.AGENT_END, # 一次 Agent 任务结束后(只读观测/上报,对应范式 Stop)
64
+ }
65
+
66
+
67
+ def normalize_hook(event: str) -> str:
68
+ """把旧钩子名规整为标准名;已是标准名则原样返回。"""
69
+ return ALIAS.get(event, event)
70
+
71
+
72
+ def is_valid_hook(event: str) -> bool:
73
+ return normalize_hook(event) in ALL_HOOKS
@@ -0,0 +1,278 @@
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
+
22
+ # ============ 断路器 ============
23
+
24
+ CircuitState = Literal["closed", "open", "half_open"]
25
+
26
+
27
+ @dataclass
28
+ class _BreakerEntry:
29
+ failures: int = 0 # closed 态的连续失败计数
30
+ state: CircuitState = "closed"
31
+ opened_at: float = 0.0 # 进入 open 的时刻(用于冷却计时)
32
+ probe_in_flight: bool = False # half_open 态:是否已有探针在执行(只放一个)
33
+ probe_started_at: float = 0.0 # 探针放行的时刻(用于探针超时自愈)
34
+ probe_successes: int = 0 # half_open 态:已累计的探针成功数
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() # allow/record 可能跨线程,需加锁
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
+ # 冷却满 → 真正进入 half_open,放行第一个探针
75
+ e.state = "half_open"
76
+ e.probe_in_flight = True
77
+ e.probe_started_at = time.time()
78
+ e.probe_successes = 0
79
+ return True
80
+ return False
81
+ # half_open:已有探针在飞 → 拒绝其余;否则放行一个新探针
82
+ if e.state == "half_open":
83
+ if e.probe_in_flight:
84
+ # 探针超时未记账(调用方崩溃/漏记)→ 视为丢失,释放以免永久卡死
85
+ if time.time() - e.probe_started_at >= self.probe_timeout:
86
+ e.probe_in_flight = False
87
+ else:
88
+ return False
89
+ e.probe_in_flight = True
90
+ e.probe_started_at = time.time()
91
+ return True
92
+ return False
93
+
94
+ def record_success(self, sid: str):
95
+ with self._lock:
96
+ e = self._get(sid)
97
+ if e.state == "half_open":
98
+ e.probe_in_flight = False
99
+ e.probe_successes += 1
100
+ if e.probe_successes >= self.recovery_threshold:
101
+ # 探针连续成功达标 → 恢复
102
+ e.state = "closed"
103
+ e.failures = 0
104
+ e.probe_successes = 0
105
+ # 否则保持 half_open,等下一个探针
106
+ else:
107
+ e.failures = 0
108
+ e.state = "closed"
109
+
110
+ def record_failure(self, sid: str):
111
+ with self._lock:
112
+ e = self._get(sid)
113
+ if e.state == "half_open":
114
+ # 探针失败 → 回到 open,重新计冷却
115
+ e.state = "open"
116
+ e.opened_at = time.time()
117
+ e.probe_in_flight = False
118
+ e.probe_successes = 0
119
+ return
120
+ e.failures += 1
121
+ if e.failures >= self.threshold:
122
+ e.state = "open"
123
+ e.opened_at = time.time()
124
+
125
+ def status(self, sid: str) -> dict:
126
+ with self._lock:
127
+ e = self._get(sid)
128
+ state = e.state
129
+ # open 但冷却已满 → 对外显示 half_open(下次 allow 会真正转入)
130
+ if e.state == "open" and time.time() - e.opened_at >= self.cooldown:
131
+ state = "half_open"
132
+ return {"state": state, "failures": e.failures,
133
+ "threshold": self.threshold,
134
+ "probe_successes": e.probe_successes,
135
+ "recovery_threshold": self.recovery_threshold}
136
+
137
+ def is_open(self, sid: str) -> bool:
138
+ """只读查询:该 skill 当前是否处于「阻止调用」状态。
139
+
140
+ 【P4-0】原实现写的是 `state == "open" and not self.allow(sid)` —— 但 allow() 在
141
+ 「open 且冷却已满」时会**副作用性地**转入 half_open 并占掉唯一的探针名额。于是
142
+ 「只想看一眼断路器开着没」的监控代码会意外消耗掉恢复探测机会,把真正该放行的探针
143
+ 挡在外面。这里改成纯读,不做任何状态迁移。
144
+
145
+ 语义:open 且冷却未满 → 阻止;open 且冷却已满 → 视作「下次调用会被放行」,不计为打开;
146
+ half_open 仅当探针在飞且未超时时计入。
147
+ """
148
+ with self._lock:
149
+ e = self._get(sid)
150
+ if e.state == "open":
151
+ return (time.time() - e.opened_at) < self.cooldown
152
+ if e.state == "half_open":
153
+ return e.probe_in_flight and (time.time() - e.probe_started_at) < self.probe_timeout
154
+ return False
155
+
156
+
157
+ # ============ 生命周期钩子 ============
158
+
159
+ HookEvent = Literal["before_tool_call", "after_tool_call", "on_error", "on_loop",
160
+ "session_start", "agent_start", "agent_end", "session_end",
161
+ "before_message_write",
162
+ # 向后兼容的旧名 + E2 语义别名(内部规整为标准名)
163
+ "pre_skill", "post_skill", "pre_reasoning", "task_complete"]
164
+
165
+
166
+ @dataclass
167
+ class Hook:
168
+ event: HookEvent
169
+ matcher: str # skill id / glob;'*' 匹配全部
170
+ handler: Callable # 宿主提供的处理函数
171
+ name: str = ""
172
+
173
+ def matches(self, skill_id: str) -> bool:
174
+ return fnmatch.fnmatch(skill_id or "", self.matcher)
175
+
176
+
177
+ @dataclass
178
+ class HookResult:
179
+ blocked: bool = False # 仅 pre_skill 可阻断
180
+ reason: str = ""
181
+ modified_args: Optional[dict] = None
182
+ additional_context: Optional[str] = None # E2:CONTEXT_HOOKS(pre_reasoning/agent_start)注入的推理上下文
183
+
184
+
185
+ class HookManager:
186
+ """生命周期钩子管理。钩子超时受限、容错、可审计。
187
+
188
+ 规则(对齐 FRAMEWORK.md):
189
+ - 超时受限:单个 hook 超过 timeout 视为出错(不阻塞链路)
190
+ - 容错:hook 失败不中断 skill 链(顾问不是控制器)
191
+ - 例外:pre_skill 显式返回 {block:True} 可阻断(这是它被授予的权力)
192
+ - 可审计:每次 hook 执行记录到 audit_log
193
+ """
194
+
195
+ def __init__(self, timeout: float = 10.0, audit_limit: int = 2000):
196
+ self.timeout = timeout
197
+ self._hooks: list[Hook] = []
198
+ # 【深审修复·长期运行泄漏】audit_log 原为普通 list、只增不减:每次 skill invoke 触发
199
+ # pre/post/error/loop 等钩子都 append,长跑(尤其 V2 管线内 dispatch 高频用工具)无界
200
+ # 增长 → 内存泄漏(与 TruthCrystallizer._pending 同类)。改有界 deque:保留最近 N 条,
201
+ # 溢出自动丢弃最老的;deque.append 原子 ⇒ 并发 fire 也线程安全,无需额外锁。
202
+ self.audit_log: deque = deque(maxlen=max(100, audit_limit))
203
+
204
+ def register(self, event: HookEvent, handler: Callable,
205
+ matcher: str = "*", name: str = ""):
206
+ from .hooks_spec import normalize_hook, is_valid_hook
207
+ norm = normalize_hook(event) # 旧名(pre_skill)→ 标准名(before_tool_call)
208
+ if not is_valid_hook(norm):
209
+ # 非法钩子名(可能拼写错误)→ 记录警告,避免静默失效难以排查
210
+ self.audit_log.append({
211
+ "event": event, "hook": name or getattr(handler, "__name__", "?"),
212
+ "matcher": matcher, "duration_ms": 0, "timed_out": False,
213
+ "error": f"未知钩子点「{event}」,该钩子永不触发(请检查拼写)",
214
+ })
215
+ self._hooks.append(Hook(norm, matcher, handler, name or handler.__name__))
216
+
217
+ def _run_one(self, hook: Hook, ctx: dict) -> dict:
218
+ """执行单个 hook,计时 + 容错。返回 hook 的返回值(dict)或 {}。"""
219
+ t0 = time.time()
220
+ outcome, err = {}, None
221
+ try:
222
+ ret = hook.handler(ctx)
223
+ if isinstance(ret, dict):
224
+ outcome = ret
225
+ except Exception as e:
226
+ err = str(e)
227
+ dt = time.time() - t0
228
+ # 超时只记录(同步 hook 无法真正中断,但记录超时供审计)
229
+ timed_out = dt > self.timeout
230
+ self.audit_log.append({
231
+ "event": hook.event, "hook": hook.name, "matcher": hook.matcher,
232
+ "duration_ms": round(dt * 1000, 1), "timed_out": timed_out,
233
+ "error": err,
234
+ })
235
+ return {} if err else outcome
236
+
237
+ def fire(self, event: HookEvent, skill_id: str = "", ctx: dict = None) -> HookResult:
238
+ """触发某事件的所有匹配 hook。仅 before_tool_call(原 pre_skill)的 block 生效;
239
+ CONTEXT_HOOKS(pre_reasoning/agent_start)返回的 additional_context 会被收集拼接。
240
+
241
+ 【P4-3】⚠ ctx 是**浅拷贝**(dict(...)),不是深拷贝。hook 不得原地修改 ctx 里的嵌套
242
+ dict(如 ctx["args"]["x"])——那会连带改掉调用方的原对象。要改参数请用返回值
243
+ `{"modified_args": {...}}` 整块替换(仅 before_tool_call 生效)。此处刻意不做 deepcopy,
244
+ 以免每次工具调用都付深拷贝开销;约束靠本注释 + 钩子约定维持。
245
+ """
246
+ from .hooks_spec import (normalize_hook, BLOCKING_HOOKS, TOOL_SCOPED_HOOKS,
247
+ CONTEXT_HOOKS)
248
+ event = normalize_hook(event) # 旧名/标准名统一
249
+ ctx = dict(ctx or {}, event=event, skill_id=skill_id)
250
+ result = HookResult()
251
+ ctx_parts = []
252
+ for hook in self._hooks:
253
+ if hook.event != event:
254
+ continue
255
+ # 工具级钩子按 skill_id 过滤(matcher)
256
+ if event in TOOL_SCOPED_HOOKS and not hook.matches(skill_id):
257
+ continue
258
+ out = self._run_one(hook, ctx)
259
+ # 仅有阻断权的钩子(before_tool_call)的 block / modified_args 生效
260
+ if event in BLOCKING_HOOKS:
261
+ if out.get("block"):
262
+ result.blocked = True
263
+ result.reason = out.get("reason", f"被 {event} 钩子阻断")
264
+ return result
265
+ if out.get("modified_args") is not None:
266
+ result.modified_args = out["modified_args"]
267
+ # 可注入上下文的钩子:收集 additional_context(兼容驼峰 additionalContext)
268
+ if event in CONTEXT_HOOKS:
269
+ inj = out.get("additional_context") or out.get("additionalContext")
270
+ if inj:
271
+ ctx_parts.append(str(inj))
272
+ if ctx_parts:
273
+ result.additional_context = "\n".join(ctx_parts)
274
+ return result
275
+
276
+ def recent_audit(self, n: int = 20) -> list:
277
+ # deque 不支持负索引切片,先快照成 list(原子快照,并发安全)再取尾 N 条
278
+ return list(self.audit_log)[-n:]
@@ -0,0 +1,174 @@
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
+
8
+
9
+ @dataclass
10
+ class MarkdownSkill:
11
+ id: str
12
+ name: str
13
+ description: str
14
+ triggers: list = field(default_factory=list)
15
+ body: str = ""
16
+ enabled: bool = True
17
+ source: str = "user"
18
+ version: str = "" # OpenClaw 包的版本(_meta.json/frontmatter),可空
19
+ builtin: bool = False # 随包分发的内置技能(repo skills/*/SKILL.md)
20
+
21
+ def to_dict(self) -> dict:
22
+ d = asdict(self); d["trust"] = "low"; return d # 指令注入=提示词操纵面,信任=低
23
+
24
+ def to_meta(self) -> dict:
25
+ d = self.to_dict(); d.pop("body", None); return d
26
+
27
+
28
+ def _slug(s: str) -> str:
29
+ return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_") or "skill"
30
+
31
+
32
+ def parse_skill_md(text: str, id: str = None) -> MarkdownSkill:
33
+ """解析 SKILL.md(--- yaml frontmatter --- + 正文)。缺 name/description → ValueError。"""
34
+ text = (text or "").lstrip("").strip()
35
+ m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
36
+ if not m:
37
+ raise ValueError("缺少 --- frontmatter ---")
38
+ try:
39
+ fm = yaml.safe_load(m.group(1)) or {}
40
+ except Exception as e:
41
+ raise ValueError(f"frontmatter 非法 YAML: {e}")
42
+ if not isinstance(fm, dict):
43
+ raise ValueError("frontmatter 需为键值对")
44
+ name = str(fm.get("name") or "").strip()
45
+ desc = str(fm.get("description") or "").strip()
46
+ if not name or not desc:
47
+ raise ValueError("SKILL.md 必须含 name 和 description")
48
+ trig = fm.get("triggers") or []
49
+ if isinstance(trig, str):
50
+ trig = [t.strip() for t in trig.split(",") if t.strip()]
51
+ trig = [str(t) for t in trig]
52
+ builtin = bool(fm.get("builtin", False))
53
+ return MarkdownSkill(id=id or _slug(name), name=name, description=desc,
54
+ triggers=trig, body=m.group(2).strip(),
55
+ source="builtin" if builtin else "user",
56
+ version=str(fm.get("version") or "").strip(),
57
+ builtin=builtin)
58
+
59
+
60
+ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
61
+ """加载随包分发的指令技能:`<root>/<name>/SKILL.md` 中 **frontmatter 标了 builtin: true** 的。
62
+
63
+ 为什么只认 builtin 标记:仓库 `skills/` 下还有 `execution-contract` 之类的**开发者文档**,
64
+ 全量注入会把每轮 prompt 撑肿。显式标记 = 只把"真给模型执行的指令"注入。
65
+ 用户已安装的同 id 技能优先(不覆盖),内置只是缺省值。
66
+ """
67
+ from pathlib import Path
68
+ loaded = []
69
+ try:
70
+ base = Path(root)
71
+ if not base.is_dir():
72
+ return loaded
73
+ for p in sorted(base.glob("*/SKILL.md")):
74
+ try:
75
+ s = parse_skill_md(p.read_text(encoding="utf-8", errors="replace"))
76
+ except Exception:
77
+ continue # 单个技能坏掉不影响其余
78
+ if not s.builtin or reg.get(s.id) is not None:
79
+ continue
80
+ reg.register(s)
81
+ loaded.append(s.id)
82
+ except Exception:
83
+ pass
84
+ return loaded
85
+
86
+
87
+ class MarkdownSkillRegistry:
88
+ def __init__(self):
89
+ self._skills = {}
90
+ self._lock = threading.RLock()
91
+ self.policy = None # 全局封杀策略(宿主注入);被封的不参与注入
92
+
93
+ def register(self, s: MarkdownSkill):
94
+ with self._lock:
95
+ self._skills[s.id] = s
96
+
97
+ def unregister(self, sid: str) -> bool:
98
+ with self._lock:
99
+ return self._skills.pop(sid, None) is not None
100
+
101
+ def get(self, sid: str):
102
+ with self._lock:
103
+ return self._skills.get(sid)
104
+
105
+ def all(self) -> list:
106
+ with self._lock:
107
+ return list(self._skills.values())
108
+
109
+ def catalog(self) -> list:
110
+ return [s.to_meta() for s in self.all()]
111
+
112
+ def match(self, task: str) -> list:
113
+ """有 triggers → 按 triggers(子串);无 triggers(原味 OpenClaw)→ 按 name+description 描述相关。"""
114
+ t_raw = task or ""
115
+ t_low = t_raw.lower()
116
+ out = []
117
+ pol = self.policy
118
+ for s in self.all():
119
+ if not s.enabled:
120
+ continue
121
+ if pol is not None and pol.is_blocked(s.id):
122
+ continue # 被封杀 → 不注入(kill-switch)
123
+ if s.triggers:
124
+ if any(k.lower() in t_low for k in s.triggers):
125
+ out.append(s)
126
+ elif _desc_relevant(s, t_low, t_raw):
127
+ out.append(s)
128
+ return out
129
+
130
+
131
+ _ASCII_WORD = re.compile(r"[a-z0-9]{3,}")
132
+ _CJK_RUN = re.compile(r"[一-鿿]{2,}")
133
+
134
+
135
+ def _desc_relevant(skill, t_low: str, t_raw: str) -> bool:
136
+ """无 triggers 时:技能 name+description 与任务是否关键词重叠。
137
+ ASCII 词按词边界(避免 knowledge⊃now 式误命中);中文按 2-gram 子串。"""
138
+ blob = f"{skill.name} {skill.description}"
139
+ for w in set(_ASCII_WORD.findall(blob.lower())):
140
+ if re.search(rf"\b{re.escape(w)}\b", t_low):
141
+ return True
142
+ for run in _CJK_RUN.findall(blob):
143
+ for i in range(len(run) - 1):
144
+ if run[i:i + 2] in t_raw:
145
+ return True
146
+ return False
147
+
148
+
149
+ def extract_skill_package(zip_bytes: bytes, max_uncompressed: int = 2_000_000):
150
+ """从 OpenClaw 包(zip)按名读取 SKILL.md(+可选 _meta.json)。
151
+ 仅按名 read,不 extractall → 无 zip-slip;累计解压超上限 → 防 zip bomb。
152
+ 返回 (skill_md_text, meta_dict)。找不到 SKILL.md → ValueError。"""
153
+ import io
154
+ import json as _json
155
+ import zipfile
156
+ try:
157
+ zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
158
+ except Exception as e:
159
+ raise ValueError(f"不是合法 zip: {e}")
160
+ total = sum(i.file_size for i in zf.infolist())
161
+ if total > max_uncompressed:
162
+ raise ValueError(f"解压体积超上限({total} > {max_uncompressed})")
163
+ skill_name = next((n for n in zf.namelist() if n.lower().endswith("skill.md")), None)
164
+ if not skill_name:
165
+ raise ValueError("包内未找到 SKILL.md")
166
+ skill_md = zf.read(skill_name).decode("utf-8", "replace")
167
+ meta = {}
168
+ meta_name = next((n for n in zf.namelist() if n.lower().endswith("_meta.json")), None)
169
+ if meta_name:
170
+ try:
171
+ meta = _json.loads(zf.read(meta_name).decode("utf-8", "replace")) or {}
172
+ except Exception:
173
+ meta = {}
174
+ 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