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.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +115 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +144 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1436 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
横切机制:断路器 + 生命周期钩子 —— AHS FRAMEWORK.md 的代码落地。
|
|
3
|
+
|
|
4
|
+
CircuitBreaker 每个 skill 独立计数,连续失败 N 次 → OPEN → 停止重试 → 降级
|
|
5
|
+
HookManager pre_skill/post_skill/on_error/session_start/session_end +
|
|
6
|
+
agent_start(别名 pre_reasoning,可注入 additionalContext)/
|
|
7
|
+
agent_end(别名 task_complete,只读)——认知管线 run_task 触发(E2)
|
|
8
|
+
超时受限、容错、可审计;hook 是顾问不是控制器
|
|
9
|
+
(例外:pre_skill 显式返回 deny 可阻断)
|
|
10
|
+
|
|
11
|
+
对应 docs/internal/HARNESS_STANDARD.md 第二期(稳健性)。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import time
|
|
15
|
+
import fnmatch
|
|
16
|
+
import threading
|
|
17
|
+
from collections import deque
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Callable, Optional, Literal
|
|
20
|
+
|
|
21
|
+
from .hooks_spec import HookEvent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
CircuitState = Literal["closed", "open", "half_open"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class _BreakerEntry:
|
|
29
|
+
failures: int = 0
|
|
30
|
+
state: CircuitState = "closed"
|
|
31
|
+
opened_at: float = 0.0
|
|
32
|
+
probe_in_flight: bool = False
|
|
33
|
+
probe_started_at: float = 0.0
|
|
34
|
+
probe_successes: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CircuitBreaker:
|
|
38
|
+
"""每个 skill 独立的断路器。标准三态机:closed / open / half_open。
|
|
39
|
+
|
|
40
|
+
- closed:正常放行。连续失败 ≥ threshold → 转 open
|
|
41
|
+
- open:停止调用。冷却 cooldown 秒后 → 转 half_open
|
|
42
|
+
- half_open:只放行 ONE 个探针请求(其余仍拒绝);
|
|
43
|
+
探针成功累计达 recovery_threshold → 转 closed(恢复);
|
|
44
|
+
任一探针失败 → 转回 open,重新计冷却
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, threshold: int = 3, cooldown: float = 30.0,
|
|
48
|
+
recovery_threshold: int = 1, probe_timeout: float = 60.0):
|
|
49
|
+
self.threshold = threshold
|
|
50
|
+
self.cooldown = cooldown
|
|
51
|
+
self.recovery_threshold = max(1, recovery_threshold)
|
|
52
|
+
self.probe_timeout = probe_timeout
|
|
53
|
+
self._entries: dict[str, _BreakerEntry] = {}
|
|
54
|
+
self._lock = threading.RLock()
|
|
55
|
+
|
|
56
|
+
def _get(self, sid: str) -> _BreakerEntry:
|
|
57
|
+
if sid not in self._entries:
|
|
58
|
+
self._entries[sid] = _BreakerEntry()
|
|
59
|
+
return self._entries[sid]
|
|
60
|
+
|
|
61
|
+
def allow(self, sid: str) -> bool:
|
|
62
|
+
"""该 skill 现在是否允许调用。
|
|
63
|
+
|
|
64
|
+
closed → True;open(冷却中)→ False;
|
|
65
|
+
open(冷却满)→ 转 half_open 并放行这一个探针;
|
|
66
|
+
half_open → 仅当没有探针在飞时放行一个,否则 False。
|
|
67
|
+
"""
|
|
68
|
+
with self._lock:
|
|
69
|
+
e = self._get(sid)
|
|
70
|
+
if e.state == "closed":
|
|
71
|
+
return True
|
|
72
|
+
if e.state == "open":
|
|
73
|
+
if time.time() - e.opened_at >= self.cooldown:
|
|
74
|
+
e.state = "half_open"
|
|
75
|
+
e.probe_in_flight = True
|
|
76
|
+
e.probe_started_at = time.time()
|
|
77
|
+
e.probe_successes = 0
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
if e.state == "half_open":
|
|
81
|
+
if e.probe_in_flight:
|
|
82
|
+
if time.time() - e.probe_started_at >= self.probe_timeout:
|
|
83
|
+
e.probe_in_flight = False
|
|
84
|
+
else:
|
|
85
|
+
return False
|
|
86
|
+
e.probe_in_flight = True
|
|
87
|
+
e.probe_started_at = time.time()
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def record_success(self, sid: str):
|
|
92
|
+
with self._lock:
|
|
93
|
+
e = self._get(sid)
|
|
94
|
+
if e.state == "half_open":
|
|
95
|
+
e.probe_in_flight = False
|
|
96
|
+
e.probe_successes += 1
|
|
97
|
+
if e.probe_successes >= self.recovery_threshold:
|
|
98
|
+
e.state = "closed"
|
|
99
|
+
e.failures = 0
|
|
100
|
+
e.probe_successes = 0
|
|
101
|
+
else:
|
|
102
|
+
e.failures = 0
|
|
103
|
+
e.state = "closed"
|
|
104
|
+
|
|
105
|
+
def record_failure(self, sid: str):
|
|
106
|
+
with self._lock:
|
|
107
|
+
e = self._get(sid)
|
|
108
|
+
if e.state == "half_open":
|
|
109
|
+
e.state = "open"
|
|
110
|
+
e.opened_at = time.time()
|
|
111
|
+
e.probe_in_flight = False
|
|
112
|
+
e.probe_successes = 0
|
|
113
|
+
return
|
|
114
|
+
e.failures += 1
|
|
115
|
+
if e.failures >= self.threshold:
|
|
116
|
+
e.state = "open"
|
|
117
|
+
e.opened_at = time.time()
|
|
118
|
+
|
|
119
|
+
def status(self, sid: str) -> dict:
|
|
120
|
+
with self._lock:
|
|
121
|
+
e = self._get(sid)
|
|
122
|
+
state = e.state
|
|
123
|
+
if e.state == "open" and time.time() - e.opened_at >= self.cooldown:
|
|
124
|
+
state = "half_open"
|
|
125
|
+
return {"state": state, "failures": e.failures,
|
|
126
|
+
"threshold": self.threshold,
|
|
127
|
+
"probe_successes": e.probe_successes,
|
|
128
|
+
"recovery_threshold": self.recovery_threshold}
|
|
129
|
+
|
|
130
|
+
def is_open(self, sid: str) -> bool:
|
|
131
|
+
"""只读查询:该 skill 当前是否处于「阻止调用」状态。
|
|
132
|
+
|
|
133
|
+
【P4-0】原实现写的是 `state == "open" and not self.allow(sid)` —— 但 allow() 在
|
|
134
|
+
「open 且冷却已满」时会**副作用性地**转入 half_open 并占掉唯一的探针名额。于是
|
|
135
|
+
「只想看一眼断路器开着没」的监控代码会意外消耗掉恢复探测机会,把真正该放行的探针
|
|
136
|
+
挡在外面。这里改成纯读,不做任何状态迁移。
|
|
137
|
+
|
|
138
|
+
语义:open 且冷却未满 → 阻止;open 且冷却已满 → 视作「下次调用会被放行」,不计为打开;
|
|
139
|
+
half_open 仅当探针在飞且未超时时计入。
|
|
140
|
+
"""
|
|
141
|
+
with self._lock:
|
|
142
|
+
e = self._get(sid)
|
|
143
|
+
if e.state == "open":
|
|
144
|
+
return (time.time() - e.opened_at) < self.cooldown
|
|
145
|
+
if e.state == "half_open":
|
|
146
|
+
return e.probe_in_flight and (time.time() - e.probe_started_at) < self.probe_timeout
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Hook:
|
|
152
|
+
event: HookEvent
|
|
153
|
+
matcher: str
|
|
154
|
+
handler: Callable
|
|
155
|
+
name: str = ""
|
|
156
|
+
|
|
157
|
+
def matches(self, skill_id: str) -> bool:
|
|
158
|
+
return fnmatch.fnmatch(skill_id or "", self.matcher)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class HookResult:
|
|
163
|
+
blocked: bool = False
|
|
164
|
+
reason: str = ""
|
|
165
|
+
modified_args: Optional[dict] = None
|
|
166
|
+
additional_context: Optional[str] = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class HookManager:
|
|
170
|
+
"""生命周期钩子管理。钩子超时受限、容错、可审计。
|
|
171
|
+
|
|
172
|
+
规则(对齐 FRAMEWORK.md):
|
|
173
|
+
- 超时受限:单个 hook 超过 timeout 视为出错(不阻塞链路)
|
|
174
|
+
- 容错:hook 失败不中断 skill 链(顾问不是控制器)
|
|
175
|
+
- 例外:pre_skill 显式返回 {block:True} 可阻断(这是它被授予的权力)
|
|
176
|
+
- 可审计:每次 hook 执行记录到 audit_log
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, timeout: float = 10.0, audit_limit: int = 2000):
|
|
180
|
+
self.timeout = timeout
|
|
181
|
+
self._hooks: list[Hook] = []
|
|
182
|
+
self.audit_log: deque = deque(maxlen=max(100, audit_limit))
|
|
183
|
+
|
|
184
|
+
def register(self, event: HookEvent, handler: Callable,
|
|
185
|
+
matcher: str = "*", name: str = ""):
|
|
186
|
+
from .hooks_spec import normalize_hook, is_valid_hook
|
|
187
|
+
norm = normalize_hook(event)
|
|
188
|
+
if not is_valid_hook(norm):
|
|
189
|
+
self.audit_log.append({
|
|
190
|
+
"event": event, "hook": name or getattr(handler, "__name__", "?"),
|
|
191
|
+
"matcher": matcher, "duration_ms": 0, "timed_out": False,
|
|
192
|
+
"error": f"未知钩子点「{event}」,该钩子永不触发(请检查拼写)",
|
|
193
|
+
})
|
|
194
|
+
self._hooks.append(Hook(norm, matcher, handler, name or handler.__name__))
|
|
195
|
+
|
|
196
|
+
def _run_one(self, hook: Hook, ctx: dict) -> dict:
|
|
197
|
+
"""执行单个 hook,计时 + 容错。返回 hook 的返回值(dict)或 {}。"""
|
|
198
|
+
t0 = time.time()
|
|
199
|
+
outcome, err = {}, None
|
|
200
|
+
try:
|
|
201
|
+
ret = hook.handler(ctx)
|
|
202
|
+
if isinstance(ret, dict):
|
|
203
|
+
outcome = ret
|
|
204
|
+
except Exception as e:
|
|
205
|
+
err = str(e)
|
|
206
|
+
dt = time.time() - t0
|
|
207
|
+
timed_out = dt > self.timeout
|
|
208
|
+
self.audit_log.append({
|
|
209
|
+
"event": hook.event, "hook": hook.name, "matcher": hook.matcher,
|
|
210
|
+
"duration_ms": round(dt * 1000, 1), "timed_out": timed_out,
|
|
211
|
+
"error": err,
|
|
212
|
+
})
|
|
213
|
+
return {} if err else outcome
|
|
214
|
+
|
|
215
|
+
def fire(self, event: HookEvent, skill_id: str = "", ctx: dict | None = None) -> HookResult:
|
|
216
|
+
"""触发某事件的所有匹配 hook。仅 before_tool_call(原 pre_skill)的 block 生效;
|
|
217
|
+
CONTEXT_HOOKS(pre_reasoning/agent_start)返回的 additional_context 会被收集拼接。
|
|
218
|
+
|
|
219
|
+
【P4-3】⚠ ctx 是**浅拷贝**(dict(...)),不是深拷贝。hook 不得原地修改 ctx 里的嵌套
|
|
220
|
+
dict(如 ctx["args"]["x"])——那会连带改掉调用方的原对象。要改参数请用返回值
|
|
221
|
+
`{"modified_args": {...}}` 整块替换(仅 before_tool_call 生效)。此处刻意不做 deepcopy,
|
|
222
|
+
以免每次工具调用都付深拷贝开销;约束靠本注释 + 钩子约定维持。
|
|
223
|
+
"""
|
|
224
|
+
from .hooks_spec import (normalize_hook, BLOCKING_HOOKS, TOOL_SCOPED_HOOKS,
|
|
225
|
+
CONTEXT_HOOKS)
|
|
226
|
+
event = normalize_hook(event)
|
|
227
|
+
ctx = dict(ctx or {}, event=event, skill_id=skill_id)
|
|
228
|
+
result = HookResult()
|
|
229
|
+
ctx_parts = []
|
|
230
|
+
for hook in self._hooks:
|
|
231
|
+
if hook.event != event:
|
|
232
|
+
continue
|
|
233
|
+
if event in TOOL_SCOPED_HOOKS and not hook.matches(skill_id):
|
|
234
|
+
continue
|
|
235
|
+
out = self._run_one(hook, ctx)
|
|
236
|
+
if event in BLOCKING_HOOKS:
|
|
237
|
+
if out.get("block"):
|
|
238
|
+
result.blocked = True
|
|
239
|
+
result.reason = out.get("reason", f"被 {event} 钩子阻断")
|
|
240
|
+
return result
|
|
241
|
+
if out.get("modified_args") is not None:
|
|
242
|
+
result.modified_args = out["modified_args"]
|
|
243
|
+
if event in CONTEXT_HOOKS:
|
|
244
|
+
inj = out.get("additional_context") or out.get("additionalContext")
|
|
245
|
+
if inj:
|
|
246
|
+
ctx_parts.append(str(inj))
|
|
247
|
+
if ctx_parts:
|
|
248
|
+
result.additional_context = "\n".join(ctx_parts)
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
def recent_audit(self, n: int = 20) -> list:
|
|
252
|
+
return list(self.audit_log)[-n:]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""SKILL.md 指令技能:注入 Agent 提示词的 markdown 指令(非可调用工具)。无代码执行。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
from dataclasses import dataclass, field, asdict
|
|
6
|
+
import yaml
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class MarkdownSkill:
|
|
12
|
+
id: str
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
triggers: list = field(default_factory=list)
|
|
16
|
+
body: str = ""
|
|
17
|
+
enabled: bool = True
|
|
18
|
+
source: str = "user"
|
|
19
|
+
version: str = ""
|
|
20
|
+
builtin: bool = False
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict:
|
|
23
|
+
d = asdict(self); d["trust"] = "low"; return d
|
|
24
|
+
|
|
25
|
+
def to_meta(self) -> dict:
|
|
26
|
+
d = self.to_dict(); d.pop("body", None); return d
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _slug(s: str) -> str:
|
|
30
|
+
return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_") or "skill"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_skill_md(text: str, id: str | None = None) -> MarkdownSkill:
|
|
34
|
+
"""解析 SKILL.md(--- yaml frontmatter --- + 正文)。缺 name/description → ValueError。"""
|
|
35
|
+
text = (text or "").lstrip("").strip()
|
|
36
|
+
m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
|
|
37
|
+
if not m:
|
|
38
|
+
raise ValueError("缺少 --- frontmatter ---")
|
|
39
|
+
try:
|
|
40
|
+
fm = yaml.safe_load(m.group(1)) or {}
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise ValueError(f"frontmatter 非法 YAML: {e}")
|
|
43
|
+
if not isinstance(fm, dict):
|
|
44
|
+
raise ValueError("frontmatter 需为键值对")
|
|
45
|
+
name = str(fm.get("name") or "").strip()
|
|
46
|
+
desc = str(fm.get("description") or "").strip()
|
|
47
|
+
if not name or not desc:
|
|
48
|
+
raise ValueError("SKILL.md 必须含 name 和 description")
|
|
49
|
+
trig = fm.get("triggers") or []
|
|
50
|
+
if isinstance(trig, str):
|
|
51
|
+
trig = [t.strip() for t in trig.split(",") if t.strip()]
|
|
52
|
+
trig = [str(t) for t in trig]
|
|
53
|
+
builtin = bool(fm.get("builtin", False))
|
|
54
|
+
return MarkdownSkill(id=id or _slug(name), name=name, description=desc,
|
|
55
|
+
triggers=trig, body=m.group(2).strip(),
|
|
56
|
+
source="builtin" if builtin else "user",
|
|
57
|
+
version=str(fm.get("version") or "").strip(),
|
|
58
|
+
builtin=builtin)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
|
|
62
|
+
"""加载随包分发的指令技能:`<root>/<name>/SKILL.md` 中 **frontmatter 标了 builtin: true** 的。
|
|
63
|
+
|
|
64
|
+
为什么只认 builtin 标记:仓库 `skills/` 下还有 `execution-contract` 之类的**开发者文档**,
|
|
65
|
+
全量注入会把每轮 prompt 撑肿。显式标记 = 只把"真给模型执行的指令"注入。
|
|
66
|
+
用户已安装的同 id 技能优先(不覆盖),内置只是缺省值。
|
|
67
|
+
"""
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
loaded: list[Any] = []
|
|
70
|
+
try:
|
|
71
|
+
base = Path(root)
|
|
72
|
+
if not base.is_dir():
|
|
73
|
+
return loaded
|
|
74
|
+
for p in sorted(base.glob("*/SKILL.md")):
|
|
75
|
+
try:
|
|
76
|
+
s = parse_skill_md(p.read_text(encoding="utf-8", errors="replace"))
|
|
77
|
+
except Exception:
|
|
78
|
+
continue
|
|
79
|
+
if not s.builtin or reg.get(s.id) is not None:
|
|
80
|
+
continue
|
|
81
|
+
reg.register(s)
|
|
82
|
+
loaded.append(s.id)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return loaded
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MarkdownSkillRegistry:
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._skills = {}
|
|
91
|
+
self._lock = threading.RLock()
|
|
92
|
+
self.policy = None
|
|
93
|
+
|
|
94
|
+
def register(self, s: MarkdownSkill):
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._skills[s.id] = s
|
|
97
|
+
|
|
98
|
+
def unregister(self, sid: str) -> bool:
|
|
99
|
+
with self._lock:
|
|
100
|
+
return self._skills.pop(sid, None) is not None
|
|
101
|
+
|
|
102
|
+
def get(self, sid: str):
|
|
103
|
+
with self._lock:
|
|
104
|
+
return self._skills.get(sid)
|
|
105
|
+
|
|
106
|
+
def all(self) -> list:
|
|
107
|
+
with self._lock:
|
|
108
|
+
return list(self._skills.values())
|
|
109
|
+
|
|
110
|
+
def catalog(self) -> list:
|
|
111
|
+
return [s.to_meta() for s in self.all()]
|
|
112
|
+
|
|
113
|
+
def match(self, task: str) -> list:
|
|
114
|
+
"""有 triggers → 按 triggers(子串);无 triggers(原味 OpenClaw)→ 按 name+description 描述相关。"""
|
|
115
|
+
t_raw = task or ""
|
|
116
|
+
t_low = t_raw.lower()
|
|
117
|
+
out = []
|
|
118
|
+
pol = self.policy
|
|
119
|
+
for s in self.all():
|
|
120
|
+
if not s.enabled:
|
|
121
|
+
continue
|
|
122
|
+
if pol is not None and pol.is_blocked(s.id):
|
|
123
|
+
continue
|
|
124
|
+
if s.triggers:
|
|
125
|
+
if any(k.lower() in t_low for k in s.triggers):
|
|
126
|
+
out.append(s)
|
|
127
|
+
elif _desc_relevant(s, t_low, t_raw):
|
|
128
|
+
out.append(s)
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_ASCII_WORD = re.compile(r"[a-z0-9]{3,}")
|
|
133
|
+
_CJK_RUN = re.compile(r"[一-鿿]{2,}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _desc_relevant(skill, t_low: str, t_raw: str) -> bool:
|
|
137
|
+
"""无 triggers 时:技能 name+description 与任务是否关键词重叠。
|
|
138
|
+
ASCII 词按词边界(避免 knowledge⊃now 式误命中);中文按 2-gram 子串。"""
|
|
139
|
+
blob = f"{skill.name} {skill.description}"
|
|
140
|
+
for w in set(_ASCII_WORD.findall(blob.lower())):
|
|
141
|
+
if re.search(rf"\b{re.escape(w)}\b", t_low):
|
|
142
|
+
return True
|
|
143
|
+
for run in _CJK_RUN.findall(blob):
|
|
144
|
+
for i in range(len(run) - 1):
|
|
145
|
+
if run[i:i + 2] in t_raw:
|
|
146
|
+
return True
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def extract_skill_package(zip_bytes: bytes, max_uncompressed: int = 2_000_000):
|
|
151
|
+
"""从 OpenClaw 包(zip)按名读取 SKILL.md(+可选 _meta.json)。
|
|
152
|
+
仅按名 read,不 extractall → 无 zip-slip;累计解压超上限 → 防 zip bomb。
|
|
153
|
+
返回 (skill_md_text, meta_dict)。找不到 SKILL.md → ValueError。"""
|
|
154
|
+
import io
|
|
155
|
+
import json as _json
|
|
156
|
+
import zipfile
|
|
157
|
+
try:
|
|
158
|
+
zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise ValueError(f"不是合法 zip: {e}")
|
|
161
|
+
total = sum(i.file_size for i in zf.infolist())
|
|
162
|
+
if total > max_uncompressed:
|
|
163
|
+
raise ValueError(f"解压体积超上限({total} > {max_uncompressed})")
|
|
164
|
+
skill_name = next((n for n in zf.namelist() if n.lower().endswith("skill.md")), None)
|
|
165
|
+
if not skill_name:
|
|
166
|
+
raise ValueError("包内未找到 SKILL.md")
|
|
167
|
+
skill_md = zf.read(skill_name).decode("utf-8", "replace")
|
|
168
|
+
meta: dict[str, Any] = {}
|
|
169
|
+
meta_name = next((n for n in zf.namelist() if n.lower().endswith("_meta.json")), None)
|
|
170
|
+
if meta_name:
|
|
171
|
+
try:
|
|
172
|
+
meta = _json.loads(zf.read(meta_name).decode("utf-8", "replace")) or {}
|
|
173
|
+
except Exception:
|
|
174
|
+
meta = {}
|
|
175
|
+
return skill_md, meta
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""SKILL.md 指令技能持久化。<data_dir>/markdown_skills.json。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dataclasses import fields as _fields
|
|
7
|
+
|
|
8
|
+
from .markdown_skill import MarkdownSkill, MarkdownSkillRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MarkdownStore:
|
|
12
|
+
def __init__(self, data_dir):
|
|
13
|
+
self._path = Path(data_dir) / "markdown_skills.json"
|
|
14
|
+
self._lock = threading.Lock()
|
|
15
|
+
|
|
16
|
+
def load(self, reg: MarkdownSkillRegistry):
|
|
17
|
+
if not self._path.exists():
|
|
18
|
+
return
|
|
19
|
+
try:
|
|
20
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
21
|
+
except Exception:
|
|
22
|
+
return
|
|
23
|
+
allowed = {f.name for f in _fields(MarkdownSkill)}
|
|
24
|
+
for d in data.get("skills", []):
|
|
25
|
+
try:
|
|
26
|
+
reg.register(MarkdownSkill(**{k: v for k, v in d.items() if k in allowed}))
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
def save(self, reg: MarkdownSkillRegistry):
|
|
31
|
+
with self._lock:
|
|
32
|
+
payload = {"skills": [s.to_dict() for s in reg.all()]}
|
|
33
|
+
try:
|
|
34
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
tmp = self._path.with_suffix(".tmp")
|
|
36
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
37
|
+
tmp.replace(self._path)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""MCP 连接器(HTTP Streamable/SSE 远程)。mcp SDK 是 async,本模块用后台事件循环线程
|
|
2
|
+
把它桥成同步,供 SkillAdapter(sync,跨请求/后台线程)调用。按需连接:每次开→用→关。
|
|
3
|
+
仅远程传输,无本地代码执行。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import asyncio
|
|
6
|
+
import threading
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
|
|
9
|
+
from mcp import ClientSession
|
|
10
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
11
|
+
from mcp.client.sse import sse_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Bridge:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self._loop = asyncio.new_event_loop()
|
|
17
|
+
t = threading.Thread(target=self._run, daemon=True, name="mcp-loop")
|
|
18
|
+
t.start()
|
|
19
|
+
|
|
20
|
+
def _run(self):
|
|
21
|
+
asyncio.set_event_loop(self._loop)
|
|
22
|
+
self._loop.run_forever()
|
|
23
|
+
|
|
24
|
+
def run(self, coro, timeout: float = 35):
|
|
25
|
+
fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
26
|
+
try:
|
|
27
|
+
return fut.result(timeout=timeout)
|
|
28
|
+
except BaseException:
|
|
29
|
+
fut.cancel()
|
|
30
|
+
raise
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_bridge = None
|
|
34
|
+
_bridge_lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_bridge() -> _Bridge:
|
|
38
|
+
global _bridge
|
|
39
|
+
with _bridge_lock:
|
|
40
|
+
if _bridge is None:
|
|
41
|
+
_bridge = _Bridge()
|
|
42
|
+
return _bridge
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _ssrf_request_hook(request):
|
|
46
|
+
"""httpx 请求钩子:对每个即将发出的请求(初始 + 重定向 follow-up)校验 host。
|
|
47
|
+
堵 redirect-SSRF + DNS 重绑定 + 配置漂移(恶意 server 把你导去内网/云元数据)。"""
|
|
48
|
+
from .review import _is_internal_host
|
|
49
|
+
host = (request.url.host or "").lower()
|
|
50
|
+
if await asyncio.to_thread(_is_internal_host, host):
|
|
51
|
+
raise RuntimeError(f"MCP 连接被拒:目标解析到内网/本地/元数据地址({host})")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _safe_httpx_factory(headers=None, timeout=None, auth=None):
|
|
55
|
+
import httpx
|
|
56
|
+
return httpx.AsyncClient(
|
|
57
|
+
headers=headers,
|
|
58
|
+
timeout=timeout if timeout is not None else httpx.Timeout(30.0),
|
|
59
|
+
auth=auth,
|
|
60
|
+
follow_redirects=True,
|
|
61
|
+
event_hooks={"request": [_ssrf_request_hook]},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@asynccontextmanager
|
|
66
|
+
async def _connect(url, headers, transport):
|
|
67
|
+
if transport == "sse":
|
|
68
|
+
async with sse_client(url, headers=headers, httpx_client_factory=_safe_httpx_factory) as (read, write):
|
|
69
|
+
async with ClientSession(read, write) as s:
|
|
70
|
+
await s.initialize()
|
|
71
|
+
yield s
|
|
72
|
+
else:
|
|
73
|
+
async with streamablehttp_client(url, headers=headers, httpx_client_factory=_safe_httpx_factory) as (read, write, _):
|
|
74
|
+
async with ClientSession(read, write) as s:
|
|
75
|
+
await s.initialize()
|
|
76
|
+
yield s
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _amcp_list_tools(url, headers, transport):
|
|
80
|
+
async with _connect(url, headers, transport) as s:
|
|
81
|
+
return list((await s.list_tools()).tools)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _amcp_call_tool(url, headers, transport, name, args):
|
|
85
|
+
async with _connect(url, headers, transport) as s:
|
|
86
|
+
r = await s.call_tool(name, args or {})
|
|
87
|
+
text = "".join(getattr(c, "text", "") for c in (r.content or []))
|
|
88
|
+
return {"isError": bool(getattr(r, "isError", False)), "content": text,
|
|
89
|
+
"structured": getattr(r, "structuredContent", None)}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _norm(cfg: dict) -> dict:
|
|
93
|
+
return {"id": cfg["id"], "name": cfg.get("name") or cfg["id"], "url": cfg["url"],
|
|
94
|
+
"headers": dict(cfg.get("headers") or {}),
|
|
95
|
+
"transport": cfg.get("transport") or "streamable_http"}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get(tool, key, default=None):
|
|
99
|
+
"""兼容 dict(守门人序列化后) 与 mcp.types.Tool 对象(直连回退路径)。"""
|
|
100
|
+
return tool.get(key, default) if isinstance(tool, dict) else getattr(tool, key, default)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tool_to_spec(server_id: str, tool):
|
|
104
|
+
from . import SideEffect, SkillSpec
|
|
105
|
+
ann = _get(tool, "annotations", None)
|
|
106
|
+
if isinstance(ann, dict):
|
|
107
|
+
ro = ann.get("readOnlyHint")
|
|
108
|
+
de = ann.get("destructiveHint")
|
|
109
|
+
else:
|
|
110
|
+
ro = getattr(ann, "readOnlyHint", None) if ann else None
|
|
111
|
+
de = getattr(ann, "destructiveHint", None) if ann else None
|
|
112
|
+
se: SideEffect
|
|
113
|
+
if ro is True:
|
|
114
|
+
se, destr = "read", False
|
|
115
|
+
elif de is True:
|
|
116
|
+
se, destr = "write", True
|
|
117
|
+
else:
|
|
118
|
+
se, destr = "write", False
|
|
119
|
+
tname = _get(tool, "name", "") or ""
|
|
120
|
+
return SkillSpec(
|
|
121
|
+
id=f"mcp__{server_id}__{tname}", name=tname,
|
|
122
|
+
description=_get(tool, "description", "") or "", kind="mcp",
|
|
123
|
+
mcp_server=server_id, mcp_tool=tname, side_effect=se, destructive=destr,
|
|
124
|
+
external=True, params=_get(tool, "inputSchema", {}) or {},
|
|
125
|
+
triggers=[tname], source="user",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _ClientProxy:
|
|
130
|
+
def __init__(self, mgr, sid):
|
|
131
|
+
self._mgr, self._sid = mgr, sid
|
|
132
|
+
|
|
133
|
+
def call_tool(self, tool, args):
|
|
134
|
+
cfg = self._mgr._servers.get(self._sid)
|
|
135
|
+
if not cfg:
|
|
136
|
+
raise RuntimeError(f"MCP server {self._sid} 未登记")
|
|
137
|
+
from foxclaw.core.tracing import span as _span, current_traceparent
|
|
138
|
+
with _span("mcp.call_tool", server=self._sid, tool=str(tool)):
|
|
139
|
+
headers = dict(cfg["headers"] or {})
|
|
140
|
+
tp = current_traceparent()
|
|
141
|
+
if tp:
|
|
142
|
+
headers["traceparent"] = tp
|
|
143
|
+
out = _get_bridge().run(_amcp_call_tool(cfg["url"], headers, cfg["transport"], tool, args or {}))
|
|
144
|
+
if out.get("isError"):
|
|
145
|
+
raise RuntimeError(out.get("content") or "MCP 工具返回错误")
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class McpManager:
|
|
150
|
+
"""服务器配置 + 连接。实现 .get(id) 供 SkillAdapter.mcp_clients 用。"""
|
|
151
|
+
def __init__(self):
|
|
152
|
+
self._servers: dict = {}
|
|
153
|
+
self._lock = threading.RLock()
|
|
154
|
+
|
|
155
|
+
def restore(self, cfg: dict):
|
|
156
|
+
with self._lock:
|
|
157
|
+
self._servers[cfg["id"]] = _norm(cfg)
|
|
158
|
+
|
|
159
|
+
def add_server(self, cfg: dict):
|
|
160
|
+
c = _norm(cfg)
|
|
161
|
+
tools = _get_bridge().run(_amcp_list_tools(c["url"], c["headers"], c["transport"]))
|
|
162
|
+
with self._lock:
|
|
163
|
+
self._servers[c["id"]] = c
|
|
164
|
+
return tools
|
|
165
|
+
|
|
166
|
+
def list_tools(self, sid: str):
|
|
167
|
+
with self._lock:
|
|
168
|
+
c = self._servers.get(sid)
|
|
169
|
+
if not c:
|
|
170
|
+
raise KeyError(sid)
|
|
171
|
+
return _get_bridge().run(_amcp_list_tools(c["url"], c["headers"], c["transport"]))
|
|
172
|
+
|
|
173
|
+
def remove_server(self, sid: str) -> bool:
|
|
174
|
+
with self._lock:
|
|
175
|
+
return self._servers.pop(sid, None) is not None
|
|
176
|
+
|
|
177
|
+
def servers(self) -> list:
|
|
178
|
+
with self._lock:
|
|
179
|
+
return [dict(c) for c in self._servers.values()]
|
|
180
|
+
|
|
181
|
+
def get(self, sid: str):
|
|
182
|
+
with self._lock:
|
|
183
|
+
has = sid in self._servers
|
|
184
|
+
return _ClientProxy(self, sid) if has else None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""MCP server 配置持久化。文件 <data_dir>/mcp_servers.json。
|
|
2
|
+
|
|
3
|
+
【P-Muse·第4步·安全边界】守门人模式(FOXCLAW_GUARDIAN=1)下,主进程只把
|
|
4
|
+
非敏感元数据(id/name/url/transport)写进这里,headers 凭证只在守门人进程。
|
|
5
|
+
回退路径(FOXCLAW_GUARDIAN=0,进程内直连)没有守门人保护,headers 会连同配置
|
|
6
|
+
一起落盘——save 检测到 headers 非空时会打印告警,避免"静默落盘凭证"。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class McpStore:
|
|
16
|
+
def __init__(self, data_dir):
|
|
17
|
+
self._path = Path(data_dir) / "mcp_servers.json"
|
|
18
|
+
self._lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
def load(self) -> list:
|
|
21
|
+
if not self._path.exists():
|
|
22
|
+
return []
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(self._path.read_text(encoding="utf-8")).get("servers", [])
|
|
25
|
+
except Exception:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
def save(self, servers: list):
|
|
29
|
+
with self._lock:
|
|
30
|
+
try:
|
|
31
|
+
if any(dict(s.get("headers") or {}) for s in servers):
|
|
32
|
+
print("[McpStore] ⚠ 检测到 headers 凭证,将明文落盘到 "
|
|
33
|
+
f"{self._path.name}(当前为回退路径/未启用守门人)。",
|
|
34
|
+
file=sys.stderr, flush=True)
|
|
35
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
tmp = self._path.with_suffix(".tmp")
|
|
37
|
+
tmp.write_text(json.dumps({"servers": servers}, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
38
|
+
tmp.replace(self._path)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|