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,265 @@
1
+ """
2
+ Foxclaw Core — 上下文压缩 (Compaction)
3
+
4
+ ⚠ 【预留组件·无生产接入点】(2026-07-17 V3·B1 审核标注)
5
+ 本压缩器**无任何生产调用路径**(仅 tests/test_governance.py::test_compaction 覆盖——
6
+ 说明它曾被认真实现+验证,只是没接进执行)。根因:Foxclaw 领域 agent 每次
7
+ run_task/stream_reply 的 messages 都是**单轮新建**、不累积长对话历史;channel 层的
8
+ conversation_id 是回复路由 + 记忆隔离 key(见 dispatcher),**不维护 LLM 对话历史缓冲**。
9
+ 故当前**没有会撑爆上下文窗口的长历史可压**。
10
+ 这是**能力预留**,不是在工作的机制。若未来出现长会话累积路径(如真做多轮对话记忆),
11
+ 在其 LLM 调用前接入 `Compactor.needs_compaction()` → `compact()` 即可。
12
+ —— 见 docs/internal/AGENT_HARNESS_REMEDIATION_PLAN.md「未接线清单」。
13
+
14
+ 借鉴 OpenClaw 的渐进降级压缩思路,适配 Foxclaw。长对话会撑爆上下文窗口,
15
+ 压缩把早期历史摘要化,腾出空间,同时保住关键信息(ID/数字等标识符不丢)。
16
+
17
+ 策略(渐进降级,任一级失败退到下一级,绝不崩):
18
+ 1. LLM 摘要:把早期消息交给模型生成摘要(保留标识符)
19
+ 2. 部分摘要:若含超大消息,排除它们再摘要
20
+ 3. 截断兜底:摘要都失败 → 保留首尾、中间用占位说明替代
21
+ 始终保留:系统提示 + 最近 keep_recent 轮原样(近期上下文最重要)。
22
+ """
23
+ from __future__ import annotations
24
+ import re
25
+ from dataclasses import dataclass, field
26
+
27
+
28
+ def estimate_tokens(text: str) -> int:
29
+ if not text:
30
+ return 0
31
+ cjk = len(re.findall(r"[\u4e00-\u9fff]", text))
32
+ other = len(text) - cjk
33
+ return cjk + other // 4 + 1
34
+
35
+
36
+ def estimate_messages_tokens(messages: list) -> int:
37
+ return sum(estimate_tokens(str(m.get("content", ""))) for m in messages)
38
+
39
+
40
+ _IDENTIFIER_PAT = re.compile(
41
+ r"[a-zA-Z]{2,}_[a-zA-Z0-9]{4,}|[a-f0-9]{8,}|[0-9]{6,}|¥[\d,]+(?:\.\d+)?")
42
+
43
+
44
+ def extract_identifiers(messages: list) -> list[str]:
45
+ """从消息里提取关键标识符,确保压缩后仍可引用。"""
46
+ ids = []
47
+ for m in messages:
48
+ for match in _IDENTIFIER_PAT.findall(str(m.get("content", ""))):
49
+ if match not in ids:
50
+ ids.append(match)
51
+ return ids[:50]
52
+
53
+
54
+ @dataclass
55
+ class CompactionResult:
56
+ messages: list = field(default_factory=list)
57
+ method: str = "none"
58
+ original_count: int = 0
59
+ compacted_count: int = 0
60
+ original_tokens: int = 0
61
+ compacted_tokens: int = 0
62
+ preserved_identifiers: list = field(default_factory=list)
63
+ note: str = ""
64
+
65
+ def to_dict(self) -> dict:
66
+ return {
67
+ "method": self.method,
68
+ "original_count": self.original_count,
69
+ "compacted_count": self.compacted_count,
70
+ "original_tokens": self.original_tokens,
71
+ "compacted_tokens": self.compacted_tokens,
72
+ "saved_tokens": self.original_tokens - self.compacted_tokens,
73
+ "preserved_identifiers": self.preserved_identifiers,
74
+ "note": self.note,
75
+ }
76
+
77
+
78
+ def group_tool_calls(messages: list) -> list:
79
+ """把 messages 切成**不可分割的原子组**列表。
80
+
81
+ function-calling 会话中,`assistant`(带 tool_calls)与其后续 `tool` 结果
82
+ 必须**成对保留** —— 孤立引用会被 OpenAI/Anthropic 拒绝(400:
83
+ "tool_call_id not found" / "unexpected tool role")。
84
+
85
+ 病根:原实现只按 `role == "system"` 分区、再按**条数**切 keep_recent,
86
+ 边界若切在一轮中间就会切出「有 tool 结果、但其 assistant.tool_calls
87
+ 已被摘要掉」的碎片。base._reply_with_tools 每轮追加 1 个 assistant
88
+ (带 N 个 tool_calls)+ N 个 tool 结果,故一轮内多工具时必现。
89
+ 实测:一轮 2 个工具 + keep_recent=3 → 孤立 call_2 → 送回 provider 必 400。
90
+
91
+ 本函数按「声明-结果」结构分组,调用方改为**按组**保留,保证配对完整。
92
+ """
93
+ groups: list[list[dict]] = []
94
+ buf: list[dict] = []
95
+ i = 0
96
+ n = len(messages)
97
+ while i < n:
98
+ m = messages[i]
99
+ buf.append(m)
100
+ if m.get("role") == "assistant" and (m.get("tool_calls") or []):
101
+ ids = {tc.get("id") for tc in (m.get("tool_calls") or [])}
102
+ j = i + 1
103
+ while j < n:
104
+ nxt = messages[j]
105
+ if nxt.get("role") == "tool" and nxt.get("tool_call_id") in ids:
106
+ buf.append(nxt)
107
+ j += 1
108
+ else:
109
+ break
110
+ i = j
111
+ else:
112
+ i += 1
113
+ groups.append(buf)
114
+ buf = []
115
+ return groups
116
+
117
+
118
+ class Compactor:
119
+ """上下文压缩器。超阈值时把早期历史摘要化,保留系统提示与近期消息。"""
120
+
121
+ def __init__(self, router=None, max_tokens: int = 6000,
122
+ keep_recent: int = 4, oversized_token: int = 2000):
123
+ self.router = router
124
+ self.max_tokens = max_tokens
125
+ self.keep_recent = keep_recent
126
+ self.oversized_token = oversized_token
127
+
128
+ def needs_compaction(self, messages: list) -> bool:
129
+ return estimate_messages_tokens(messages) > self.max_tokens
130
+
131
+ def compact(self, messages: list) -> CompactionResult:
132
+ """压缩消息列表。渐进降级,绝不抛异常。"""
133
+ res = CompactionResult(
134
+ original_count=len(messages),
135
+ original_tokens=estimate_messages_tokens(messages))
136
+
137
+ if not self.needs_compaction(messages):
138
+ res.messages = list(messages)
139
+ res.method = "none"
140
+ res.compacted_count = len(messages)
141
+ res.compacted_tokens = res.original_tokens
142
+ return res
143
+
144
+ system_msgs = [m for m in messages if m.get("role") == "system"]
145
+ non_system = [m for m in messages if m.get("role") != "system"]
146
+ keep = max(0, self.keep_recent)
147
+ _groups = group_tool_calls(non_system)
148
+ if keep and _groups:
149
+ recent = [m for g in _groups[-keep:] for m in g]
150
+ to_compress = [m for g in _groups[:-keep] for m in g]
151
+ elif keep:
152
+ recent, to_compress = [], non_system
153
+ else:
154
+ recent, to_compress = [], non_system
155
+ _declared = {tc.get("id") for m in (recent + to_compress)
156
+ for tc in (m.get("tool_calls") or [])}
157
+ _orphans = [m for m in (recent + to_compress)
158
+ if m.get("role") == "tool" and m.get("tool_call_id") not in _declared]
159
+ if _orphans:
160
+ res.messages = list(messages)
161
+ res.method = "skipped_unpaired_tool"
162
+ res.compacted_count = len(messages)
163
+ res.compacted_tokens = res.original_tokens
164
+ res.note = (f"压缩跳过:检测到 {len(_orphans)} 条孤立 tool 消息"
165
+ f"(其 tool_calls 声明不在保留集内),压缩会触发 provider 400。")
166
+ return res
167
+
168
+ if not to_compress:
169
+ res.messages = list(messages)
170
+ res.method = "none"
171
+ res.compacted_count = len(messages)
172
+ res.compacted_tokens = res.original_tokens
173
+ return res
174
+
175
+ res.preserved_identifiers = extract_identifiers(to_compress)
176
+
177
+ summary = None
178
+ method = "truncate"
179
+ if self.router is not None:
180
+ summary = self._try_summarize(to_compress, res.preserved_identifiers)
181
+ if summary:
182
+ method = "llm_summary"
183
+ if summary is None and self.router is not None:
184
+ trimmed = [m for m in to_compress
185
+ if estimate_tokens(str(m.get("content", ""))) <= self.oversized_token]
186
+ if trimmed and len(trimmed) < len(to_compress):
187
+ summary = self._try_summarize(trimmed, res.preserved_identifiers)
188
+ if summary:
189
+ method = "partial_summary"
190
+ if summary is None:
191
+ summary = self._truncate_fallback(to_compress, res.preserved_identifiers)
192
+ method = "truncate"
193
+
194
+ summary_msg = {"role": "system",
195
+ "content": f"【早期对话摘要(已压缩 {len(to_compress)} 条)】\n{summary}"}
196
+ compacted = system_msgs + [summary_msg] + recent
197
+ compacted_tokens = estimate_messages_tokens(compacted)
198
+
199
+ if compacted_tokens >= res.original_tokens * 0.9:
200
+ res.messages = list(messages)
201
+ res.method = "skipped_ineffective"
202
+ res.compacted_count = len(messages)
203
+ res.compacted_tokens = res.original_tokens
204
+ sys_tokens = estimate_messages_tokens(system_msgs)
205
+ res.note = (f"压缩跳过:有效压缩空间不足(系统消息占 {sys_tokens} token)。"
206
+ f"建议精简系统提示或调大上下文预算。")
207
+ return res
208
+
209
+ res.messages = compacted
210
+ res.method = method
211
+ res.compacted_count = len(res.messages)
212
+ res.compacted_tokens = compacted_tokens
213
+ return res
214
+
215
+ def maybe_compact(self, messages: list) -> tuple:
216
+ """接入用入口:需要压缩才压,返回 (消息列表, CompactionResult | None)。
217
+
218
+ 与 compact() 的差别:未超预算时不产生 CompactionResult(调用方无需判别),
219
+ 跳过/无效时返回**原始消息**而不是半成品;任何异常都降级为原样返回。
220
+ 供 agents/base.py::_compact_context 在每次 LLM 调用前挂接。
221
+ """
222
+ try:
223
+ if not self.needs_compaction(messages):
224
+ return list(messages), None
225
+ res = self.compact(messages)
226
+ if res.method == "none" or res.method.startswith("skipped"):
227
+ return list(messages), res
228
+ return res.messages, res
229
+ except Exception:
230
+ return list(messages), None
231
+
232
+ def _try_summarize(self, messages: list, identifiers: list) -> str | None:
233
+ """用 LLM 摘要,失败返回 None(交给下一级降级)。"""
234
+ try:
235
+ convo = "\n".join(
236
+ f"{m.get('role','?')}: {str(m.get('content',''))[:1000]}"
237
+ for m in messages)
238
+ id_hint = ("\n必须原样保留这些标识符: " + ", ".join(identifiers)
239
+ if identifiers else "")
240
+ sys_prompt = ("你是对话摘要助手。把以下对话压缩成简洁摘要,保留关键决策、"
241
+ "事实、未完成事项。" + id_hint)
242
+ out = self.router.complete(
243
+ system=sys_prompt,
244
+ messages=[{"role": "user", "content": f"对话内容:\n{convo}\n\n请输出摘要:"}],
245
+ max_tokens=800, agent_id="_compaction")
246
+ if out and isinstance(out, str) and len(out.strip()) > 10:
247
+ return out.strip()
248
+ except Exception:
249
+ pass
250
+ return None
251
+
252
+ def _truncate_fallback(self, messages: list, identifiers: list) -> str:
253
+ """摘要失败的兜底:保留首尾消息要点 + 关键标识符,中间略去。"""
254
+ parts = []
255
+ if messages:
256
+ first = str(messages[0].get("content", ""))[:150]
257
+ parts.append(f"对话开始: {first}")
258
+ if len(messages) > 2:
259
+ parts.append(f"(中间 {len(messages)-2} 条消息略)")
260
+ if len(messages) > 1:
261
+ last = str(messages[-1].get("content", ""))[:150]
262
+ parts.append(f"最近: {last}")
263
+ if identifiers:
264
+ parts.append("关键标识符: " + ", ".join(identifiers))
265
+ return "\n".join(parts)
@@ -0,0 +1,65 @@
1
+ """分层配置(E1 —— 范式「信任半径递减」的轻量落地)。
2
+
3
+ 三层,信任从高到低:
4
+ · policy —— 管理员/最高。锁定项**不可被下层覆盖或撤销**(对应范式 deny 铁律)。
5
+ · user —— 运行时可改(如 skill_policy.json 的 deny/allow)。
6
+ · runtime —— 环境变量 / 默认值。
7
+
8
+ 合并规则:
9
+ · 标量:高层覆盖低层(policy > user > runtime)。
10
+ · 集合(如 deny 安全规则):各层**并集,只增不减** —— 高层只能加严、不能被下层放松。
11
+
12
+ 刻意只做三层(不照搬范式六层):够 Foxclaw 用,且**立即接线**到 SkillPolicy 的 deny 治理
13
+ (管理员强制 deny 某 skill、用户不可 undeny),不是「建了没接线」的孤儿设施。
14
+ """
15
+ from __future__ import annotations
16
+ import os
17
+ import json
18
+ from pathlib import Path
19
+
20
+
21
+ class LayeredConfig:
22
+ """三层配置合并原语。持有各层 dict 的引用 —— 若某层的集合值是可变对象(如 SkillPolicy 的
23
+ user deny set),原地增删会被 merged_set/policy_locked 实时反映,无需重建。"""
24
+
25
+ def __init__(self, policy: dict | None = None, user: dict | None = None, runtime: dict | None = None):
26
+ self.policy = dict(policy or {})
27
+ self.user = dict(user or {})
28
+ self.runtime = dict(runtime or {})
29
+
30
+ def get(self, key: str, default=None):
31
+ """标量:高层优先(policy > user > runtime)。"""
32
+ for layer in (self.policy, self.user, self.runtime):
33
+ if key in layer:
34
+ return layer[key]
35
+ return default
36
+
37
+ def merged_set(self, key: str) -> set:
38
+ """集合类(deny 等):三层并集,只增不减(高层只能加严)。"""
39
+ out: set = set()
40
+ for layer in (self.policy, self.user, self.runtime):
41
+ out |= set(layer.get(key) or [])
42
+ return out
43
+
44
+ def policy_locked(self, key: str) -> set:
45
+ """policy 层锁定的项 —— 不可被下层覆盖/撤销(deny 铁律)。"""
46
+ return set(self.policy.get(key) or [])
47
+
48
+
49
+ def load_policy_layer(data_dir=None) -> dict:
50
+ """加载 policy 层(管理员锁定,与 user 层的 skill_policy.json **物理分开**):
51
+ · 环境变量 `FOXCLAW_POLICY_DENY`(逗号分隔的 skill id);
52
+ · `<data_dir>/policy.json` 的 `{"deny": [...]}`(若存在)。
53
+ 默认(两者都无)返回空 deny → 合并结果 = 现状(behavior-preserving)。"""
54
+ deny: set = set()
55
+ env = os.environ.get("FOXCLAW_POLICY_DENY", "")
56
+ deny |= {s.strip() for s in env.split(",") if s.strip()}
57
+ if data_dir:
58
+ p = Path(data_dir) / "policy.json"
59
+ if p.exists():
60
+ try:
61
+ d = json.loads(p.read_text(encoding="utf-8"))
62
+ deny |= set(d.get("deny") or [])
63
+ except Exception:
64
+ pass
65
+ return {"deny": sorted(deny)}
@@ -0,0 +1,151 @@
1
+ """关键操作的会话 nonce 二次确认(Confirmation Broker)。
2
+
3
+ **问题**:send / pay / destructive 这类动作由权限管线挡成 `ask`(需确认),但"确认"本身
4
+ 只是一个布尔——`run_task(..., _confirmed=True)` 或 `POST /api/ahs/run {confirmed:true}`
5
+ 就能放行。它没有绑定**确认的是哪一件事**:昨天确认过"给 A 发周报",今天同一个
6
+ `confirmed=true` 会连"给 B 转账"一起放行;调用方也可以无脑重发同一个 true 反复执行。
7
+
8
+ **解法**:确认变成一次性凭据——
9
+ · `issue(session, action, args)` 签发 nonce,绑定 `会话 + 动作 + 参数指纹 + 有效期`;
10
+ · 执行前 `consume(...)` 校验并**立即作废**(单次使用),参数变了指纹就对不上;
11
+ · 过期即废(默认 300s),超窗重放无效。
12
+
13
+ **边界(诚实)**:nonce 只证明"调用方看到过这次待确认",不证明"是人类按的"。真正的
14
+ 人在回路仍需渠道/UI 把人接进环路;本模块解决的是"确认对象漂移"和"确认可重放"。
15
+
16
+ 线程安全 + 有界:nonce 来自外部,无界集合等于开一个内存耗尽面。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import secrets
23
+ import threading
24
+ import time
25
+ from dataclasses import dataclass, field
26
+ from typing import Optional, Tuple
27
+
28
+ CRITICAL_SIDE_EFFECTS = ("send", "pay")
29
+
30
+ DEFAULT_TTL = 300.0
31
+
32
+
33
+ def action_fingerprint(action: str, args: dict) -> str:
34
+ """动作指纹:同名 + 同参(字典序无关)= 同一件事。
35
+
36
+ 参数进指纹是关键:确认"给 A 发"之后把 args 改成 B,指纹不匹配 → 拒绝执行,
37
+ 堵掉「确认了这件事、执行了另一件」。
38
+ """
39
+ try:
40
+ payload = json.dumps(args or {}, sort_keys=True, ensure_ascii=False, default=str)
41
+ except Exception:
42
+ payload = repr(args)
43
+ digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
44
+ return f"{action}#{digest}"
45
+
46
+
47
+ def is_critical(side_effect: str, destructive: bool = False) -> bool:
48
+ """该动作是否必须走 nonce 二次确认。"""
49
+ return bool(destructive) or (side_effect or "") in CRITICAL_SIDE_EFFECTS
50
+
51
+
52
+ @dataclass
53
+ class PendingConfirm:
54
+ """一条待确认记录。"""
55
+ nonce: str
56
+ action: str
57
+ fingerprint: str
58
+ session: str = ""
59
+ issued: float = field(default_factory=time.time)
60
+ expires: float = 0.0
61
+ used: bool = False
62
+
63
+ def to_dict(self) -> dict:
64
+ return {"nonce": self.nonce, "action": self.action,
65
+ "session": self.session, "issued": self.issued,
66
+ "expires_at": self.expires, "used": self.used}
67
+
68
+
69
+ class ConfirmationBroker:
70
+ """签发 / 校验一次性确认码。"""
71
+
72
+ def __init__(self, ttl: float = DEFAULT_TTL, max_pending: int = 256,
73
+ clock=time.time):
74
+ self.ttl = float(ttl)
75
+ self.max_pending = max(8, int(max_pending))
76
+ self._clock = clock
77
+ self._items: dict[str, PendingConfirm] = {}
78
+ self._lock = threading.Lock()
79
+
80
+ def issue(self, session: str, action: str, args: dict,
81
+ ttl: float | None = None) -> dict:
82
+ """签发一个绑定到 (会话, 动作, 参数) 的一次性确认码。"""
83
+ now = self._clock()
84
+ ttl = self.ttl if ttl is None else float(ttl)
85
+ item = PendingConfirm(
86
+ nonce=secrets.token_hex(8),
87
+ action=action or "",
88
+ fingerprint=action_fingerprint(action or "", args or {}),
89
+ session=session or "",
90
+ issued=now, expires=now + ttl)
91
+ with self._lock:
92
+ self._purge_locked(now)
93
+ if len(self._items) >= self.max_pending:
94
+ oldest = min(self._items.values(), key=lambda it: it.issued)
95
+ self._items.pop(oldest.nonce, None)
96
+ self._items[item.nonce] = item
97
+ return {"nonce": item.nonce, "action": item.action,
98
+ "expires_at": item.expires, "ttl": ttl}
99
+
100
+ def consume(self, session: str, nonce: str, action: str = "",
101
+ args: dict | None = None, now: float | None = None) -> Tuple[bool, str]:
102
+ """校验确认码并**立即作废**。返回 (是否通过, 原因码)。
103
+
104
+ 原因码:``ok`` / ``missing_nonce`` / ``unknown_nonce`` / ``already_used`` /
105
+ ``expired`` / ``session_mismatch`` / ``action_mismatch`` / ``args_mismatch``。
106
+ 会话为空(任一侧)时不校验会话——便于进程内调用;HTTP 入口务必带 session。
107
+ """
108
+ if not nonce:
109
+ return False, "missing_nonce"
110
+ now = now if now is not None else self._clock()
111
+ with self._lock:
112
+ item: Optional[PendingConfirm] = self._items.get(nonce)
113
+ if item is not None and item.expires <= now:
114
+ self._items.pop(nonce, None)
115
+ return False, "expired"
116
+ self._purge_locked(now)
117
+ if item is None:
118
+ return False, "unknown_nonce"
119
+ if item.used:
120
+ return False, "already_used"
121
+ if item.session and session and item.session != session:
122
+ return False, "session_mismatch"
123
+ if action and item.action and item.action != action:
124
+ return False, "action_mismatch"
125
+ if args is not None and item.fingerprint != action_fingerprint(item.action, args):
126
+ return False, "args_mismatch"
127
+ item.used = True
128
+ self._items.pop(nonce, None)
129
+ return True, "ok"
130
+
131
+ def pending(self, session: str = "") -> list:
132
+ """未消费且未过期的确认码(可按会话过滤),供排查/前端展示。"""
133
+ now = self._clock()
134
+ with self._lock:
135
+ self._purge_locked(now)
136
+ return [it.to_dict() for it in self._items.values()
137
+ if not it.used and (not session or it.session == session)]
138
+
139
+ def purge(self, now: float | None = None) -> int:
140
+ now = now if now is not None else self._clock()
141
+ with self._lock:
142
+ return self._purge_locked(now)
143
+
144
+ def _purge_locked(self, now: float) -> int:
145
+ dead = [k for k, it in self._items.items() if it.expires <= now]
146
+ for k in dead:
147
+ self._items.pop(k, None)
148
+ return len(dead)
149
+
150
+ def __len__(self) -> int:
151
+ return len(self._items)
@@ -0,0 +1,27 @@
1
+ """多步 dispatch 结果判定谓词(P0-1 反思重规划的前提条件)。
2
+
3
+ **为什么放在 core**:`pipeline` 的 replan stage 需要判断「上一步是否失败」,但
4
+ `pipeline` 不能反向 import `agents` —— 那会形成 `agents ↔ pipeline` 包级环
5
+ (`pipeline/supervisor_stages.py` 曾用「函数内局部 import」回避,注释里也写明了)。
6
+ 谓词下沉到这里后,两侧都只依赖 `core`,依赖方向单一:
7
+
8
+ agents ──▶ core ◀── pipeline
9
+
10
+ 纯函数、无副作用、不 import 任何上层模块。
11
+ """
12
+
13
+
14
+ def dispatch_failed(dr) -> bool:
15
+ """判断多步 dispatch 结果是否含失败/未完成步骤(触发反思重规划的前提)。"""
16
+ if not isinstance(dr, dict):
17
+ return False
18
+ result = dr.get("result") or {}
19
+ if not isinstance(result, dict):
20
+ return False
21
+ if result.get("dispatched_to") not in ("multi", "parallel"):
22
+ return False
23
+ if result.get("blocked"):
24
+ return True
25
+ steps = result.get("parallel_steps") or result.get("multi_steps") or []
26
+ return any((s.get("failed") or s.get("blocked"))
27
+ for s in steps if isinstance(s, dict))
@@ -0,0 +1,48 @@
1
+ """把 `.env` 灌进 `os.environ`,规则只有一条:**非空值优先,空值让路**。
2
+
3
+ 为什么不用 `load_dotenv()` 的默认行为(`override=False`):
4
+ 系统环境变量里只要有同名键,就会压住 `.env`。用户机器上残留一个早就失效的
5
+ 旧 key 时,现象是「我明明在 .env 填了新 key,还是 401」,而报错信息完全指向
6
+ 不了这一层 —— 排查成本极高。2026-11-08 就踩了这个:Machine 级的
7
+ DEEPSEEK_API_KEY 是失效的,`.env` 里是新 key,后端一路读到的都是旧的。
8
+
9
+ 为什么也不用 `override=True`:
10
+ `.env` / `.env.example` 里有大量**空值占位**(`ANTHROPIC_API_KEY=` 这种)。
11
+ 整份无条件覆盖会把这些键清成空串 —— 用户真在系统环境里配好的 key 被抹掉,
12
+ 从「悄悄用错」变成「悄悄清空」,更糟。
13
+
14
+ 所以折中:空值视为「未配置」,不参与覆盖;非空值一定生效,并回报它压掉了
15
+ 哪些系统变量(只回报名字,不碰值),让这类「幽灵变量」可见。
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from pathlib import Path
22
+ from typing import List
23
+
24
+
25
+ def load_env_file(path: str | Path) -> List[str]:
26
+ """读取 env 文件并写入 `os.environ`;返回被覆盖掉同名系统变量的键名列表。
27
+
28
+ env 文件不存在 → 返回空列表(不抛)。未装 python-dotenv → 由 ImportError
29
+ 冒给调用方决定降级,本函数不吞。
30
+ """
31
+ from dotenv import dotenv_values
32
+
33
+ target = Path(path)
34
+ if not target.exists():
35
+ return []
36
+
37
+ shadows: List[str] = []
38
+ for key, value in (dotenv_values(target) or {}).items():
39
+ if not key:
40
+ continue
41
+ value = (value or "").strip()
42
+ if not value:
43
+ continue
44
+ current = os.environ.get(key, "").strip()
45
+ if current and current != value:
46
+ shadows.append(key)
47
+ os.environ[key] = value
48
+ return shadows