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,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
工具权限管线 (Permission Pipeline) —— HHS 标准 skill-guard 的代码落地。
|
|
3
|
+
|
|
4
|
+
把抽象的「四阶段权限管线」实现为可运行代码:
|
|
5
|
+
Stage 1 validate —— 输入合法性(参数 schema)
|
|
6
|
+
Stage 2 rule-match —— deny > ask > allow 优先级铁律
|
|
7
|
+
Stage 3 capability —— read_only / destructive / external,Fail-closed
|
|
8
|
+
Stage 4 confirm —— 有副作用/危险操作需确认
|
|
9
|
+
|
|
10
|
+
对应 docs/internal/HARNESS_STANDARD.md 的 A 类工具权限护栏。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Literal, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Decision = Literal["allow", "ask", "deny"]
|
|
18
|
+
|
|
19
|
+
MISSING_PARAMS = "MISSING_PARAMS"
|
|
20
|
+
DESTRUCTIVE_CONFIRM = "DESTRUCTIVE_CONFIRM"
|
|
21
|
+
SIDE_EFFECT_CONFIRM = "SIDE_EFFECT_CONFIRM"
|
|
22
|
+
ASK_RULE = "ASK_RULE"
|
|
23
|
+
EXTERNAL_CONFIRM = "EXTERNAL_CONFIRM"
|
|
24
|
+
TAINT_CONFIRM = "TAINT_CONFIRM"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PermissionRules:
|
|
29
|
+
"""宿主提供的规则集。deny 优先级最高且不可覆盖。"""
|
|
30
|
+
deny: list = field(default_factory=list)
|
|
31
|
+
ask: list = field(default_factory=list)
|
|
32
|
+
allow: list = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _match(patterns: list, skill_id: str) -> bool:
|
|
36
|
+
import fnmatch
|
|
37
|
+
return any(fnmatch.fnmatch(skill_id, p) for p in patterns)
|
|
38
|
+
|
|
39
|
+
def hits_deny(self, sid: str) -> bool:
|
|
40
|
+
return self._match(self.deny, sid)
|
|
41
|
+
|
|
42
|
+
def hits_ask(self, sid: str) -> bool:
|
|
43
|
+
return self._match(self.ask, sid)
|
|
44
|
+
|
|
45
|
+
def hits_allow(self, sid: str) -> bool:
|
|
46
|
+
return self._match(self.allow, sid)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PermissionDecision:
|
|
51
|
+
decision: Decision
|
|
52
|
+
stage: int
|
|
53
|
+
reason: str = ""
|
|
54
|
+
trust_downgrade: bool = False
|
|
55
|
+
requires_confirmation: bool = False
|
|
56
|
+
reason_code: str = ""
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict:
|
|
59
|
+
return {
|
|
60
|
+
"decision": self.decision, "stage": self.stage, "reason": self.reason,
|
|
61
|
+
"trust_downgrade": self.trust_downgrade,
|
|
62
|
+
"requires_confirmation": self.requires_confirmation,
|
|
63
|
+
"reason_code": self.reason_code,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
|
|
68
|
+
confirmed: bool = False, mode: str = "strict",
|
|
69
|
+
taint_level: int = 0, taint_threshold: int = 0) -> PermissionDecision:
|
|
70
|
+
"""四阶段权限管线。返回 allow / ask / deny。
|
|
71
|
+
|
|
72
|
+
skill: SkillSpec(需有 id / side_effect / kind,及可选 destructive/external/read_only)
|
|
73
|
+
|
|
74
|
+
【P-Muse·污点门槛】taint_level = 本会话已取回数据的最高敏感度(见
|
|
75
|
+
core.injection_guard.taint_level);taint_threshold = 触发二次确认的阈值。
|
|
76
|
+
两者默认 0 = 关闭,既有调用方行为逐字不变。仅当「污点达标 且 动作为外发/副作用」时,
|
|
77
|
+
在 Stage 3 强制升级为 ask(TAINT_CONFIRM)——对齐 Muse「碰过敏感数据 → 动态提门槛」。
|
|
78
|
+
"""
|
|
79
|
+
rules = rules or PermissionRules()
|
|
80
|
+
sid = skill.id
|
|
81
|
+
|
|
82
|
+
params = getattr(skill, "params", {}) or {}
|
|
83
|
+
missing = []
|
|
84
|
+
for k, spec in params.items():
|
|
85
|
+
required = isinstance(spec, dict) and spec.get("required")
|
|
86
|
+
if required and k not in (args or {}):
|
|
87
|
+
missing.append(k)
|
|
88
|
+
if missing:
|
|
89
|
+
return PermissionDecision("ask", 1,
|
|
90
|
+
reason=f"缺少必填参数: {', '.join(missing)}",
|
|
91
|
+
requires_confirmation=True, reason_code=MISSING_PARAMS)
|
|
92
|
+
|
|
93
|
+
if rules.hits_deny(sid):
|
|
94
|
+
return PermissionDecision("deny", 2, reason=f"命中 deny 规则(不可覆盖): {sid}")
|
|
95
|
+
ask_by_rule = rules.hits_ask(sid)
|
|
96
|
+
allow_by_rule = rules.hits_allow(sid)
|
|
97
|
+
|
|
98
|
+
side_effect = getattr(skill, "side_effect", "write")
|
|
99
|
+
destructive = getattr(skill, "destructive", False)
|
|
100
|
+
external = getattr(skill, "external", False)
|
|
101
|
+
_ro = getattr(skill, "read_only", None)
|
|
102
|
+
read_only = (side_effect == "read") if _ro is None else _ro
|
|
103
|
+
_tl = getattr(skill, "trust_level", None)
|
|
104
|
+
try:
|
|
105
|
+
trust = _tl() if callable(_tl) else "low"
|
|
106
|
+
except Exception:
|
|
107
|
+
trust = "low"
|
|
108
|
+
downgrade = trust == "low"
|
|
109
|
+
|
|
110
|
+
if mode == "readonly" and not read_only:
|
|
111
|
+
return PermissionDecision("deny", 3,
|
|
112
|
+
reason="readonly 模式下禁止非只读操作", trust_downgrade=downgrade)
|
|
113
|
+
|
|
114
|
+
if destructive and not confirmed:
|
|
115
|
+
return PermissionDecision("ask", 3,
|
|
116
|
+
reason="不可逆(destructive)操作,必须用户确认",
|
|
117
|
+
trust_downgrade=downgrade, requires_confirmation=True,
|
|
118
|
+
reason_code=DESTRUCTIVE_CONFIRM)
|
|
119
|
+
|
|
120
|
+
if taint_threshold and taint_level >= taint_threshold and not confirmed:
|
|
121
|
+
if side_effect in ("write", "send", "pay") or external:
|
|
122
|
+
return PermissionDecision("ask", 3,
|
|
123
|
+
reason="会话已取回用户敏感数据,外发/副作用动作需二次确认(污点防护)",
|
|
124
|
+
trust_downgrade=downgrade, requires_confirmation=True,
|
|
125
|
+
reason_code=TAINT_CONFIRM)
|
|
126
|
+
|
|
127
|
+
if mode == "trusted":
|
|
128
|
+
return PermissionDecision("allow", 3,
|
|
129
|
+
reason="trusted 模式放行", trust_downgrade=downgrade)
|
|
130
|
+
|
|
131
|
+
needs_confirm = (
|
|
132
|
+
ask_by_rule or external or side_effect in ("write", "send", "pay")
|
|
133
|
+
)
|
|
134
|
+
if needs_confirm and not confirmed:
|
|
135
|
+
why = ("命中 ask 规则" if ask_by_rule else
|
|
136
|
+
"外部调用" if external else f"{side_effect} 副作用")
|
|
137
|
+
code = (ASK_RULE if ask_by_rule else
|
|
138
|
+
EXTERNAL_CONFIRM if external else SIDE_EFFECT_CONFIRM)
|
|
139
|
+
return PermissionDecision("ask", 4,
|
|
140
|
+
reason=f"需确认: {why}", trust_downgrade=downgrade,
|
|
141
|
+
requires_confirmation=True, reason_code=code)
|
|
142
|
+
|
|
143
|
+
reason = ("命中 allow 规则" if allow_by_rule else
|
|
144
|
+
"只读无副作用" if read_only else "已确认")
|
|
145
|
+
return PermissionDecision("allow", 4, reason=reason, trust_downgrade=downgrade)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AHS 执行链 (Execution Pipeline) —— Agent Harness Skills 标准的全面落地。
|
|
3
|
+
|
|
4
|
+
把 6 个元 skill 串成一条可运行的执行链:
|
|
5
|
+
|
|
6
|
+
task → [router 选主skill] → 复杂? → [planner 拆解]
|
|
7
|
+
→ [composition 编排多skill]
|
|
8
|
+
每步 skill 执行都过 [guard 权限] + [breaker 断路] + [hooks 钩子]
|
|
9
|
+
全程 IO 遵循 [execution-contract],结果可审计
|
|
10
|
+
|
|
11
|
+
router / planner / composition 在这里实现;guard / breaker / hooks 复用
|
|
12
|
+
permission.py + lifecycle.py;contract 复用 SkillResult。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import asyncio
|
|
16
|
+
import inspect
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from ..planning import planner as P
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class RouteResult:
|
|
26
|
+
primary: Optional[str] = None
|
|
27
|
+
confidence: float = 0.0
|
|
28
|
+
candidates: list = field(default_factory=list)
|
|
29
|
+
fallback: Optional[str] = None
|
|
30
|
+
needs_composition: bool = False
|
|
31
|
+
reason: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def route(task: str, registry, fallback_id: str | None = None) -> RouteResult:
|
|
35
|
+
"""skill-router:把 skill 适配成通用候选,交统一引擎 trigger_route 打分选择。
|
|
36
|
+
算法(否定过滤触发命中 0.8 + 描述领域匹配 0.2)现由 foxclaw.planning 单一真源提供。"""
|
|
37
|
+
skills = registry.all()
|
|
38
|
+
if not skills:
|
|
39
|
+
return RouteResult(reason="注册表为空")
|
|
40
|
+
cands = [P.ScoredCandidate(
|
|
41
|
+
id=s.id,
|
|
42
|
+
triggers=getattr(s, "triggers", []) or [],
|
|
43
|
+
description=getattr(s, "description", "") or "")
|
|
44
|
+
for s in skills]
|
|
45
|
+
r = P.trigger_route(task, cands, fallback_id=fallback_id)
|
|
46
|
+
return RouteResult(primary=r.primary, confidence=r.confidence,
|
|
47
|
+
candidates=r.candidates, fallback=r.fallback,
|
|
48
|
+
needs_composition=r.needs_composition, reason=r.reason)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PlanTask:
|
|
53
|
+
id: str
|
|
54
|
+
name: str
|
|
55
|
+
suggested_skill: Optional[str] = None
|
|
56
|
+
dependencies: list = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Plan:
|
|
61
|
+
intent: str
|
|
62
|
+
tasks: list = field(default_factory=list)
|
|
63
|
+
flow_type: str = "sequential"
|
|
64
|
+
steps: list = field(default_factory=list)
|
|
65
|
+
needs_input: bool = False
|
|
66
|
+
missing: list = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def plan(task: str, registry) -> Plan:
|
|
70
|
+
"""planner:把任务拆成结构化计划。拆分/并行标记与算法由 foxclaw.planning 单一真源提供。
|
|
71
|
+
|
|
72
|
+
- 含并行连接词(同时/分别/并行)→ 拆成**无依赖**子任务,flow_type=parallel;
|
|
73
|
+
- 含顺序连接词(然后/接着…)→ 拆成**线性依赖链**,flow_type=sequential;
|
|
74
|
+
- 都没有 → 单步。
|
|
75
|
+
依赖关系决定能否并行:parallel 的子任务 dependencies 为空,sequential 的链式依赖前一步。
|
|
76
|
+
"""
|
|
77
|
+
task = (task or "").strip()
|
|
78
|
+
if not task:
|
|
79
|
+
return Plan(intent="", needs_input=True, missing=["task"])
|
|
80
|
+
|
|
81
|
+
if any(k in task for k in P.PARALLEL_TRIGGERS):
|
|
82
|
+
parts = P.split_on(task, P.PARALLEL_TRIGGERS + P.LIST_SEPS)
|
|
83
|
+
flow = "parallel" if len(parts) > 1 else "sequential"
|
|
84
|
+
else:
|
|
85
|
+
parts = P.split_on(task, P.SPLIT_MARKERS)
|
|
86
|
+
flow = "sequential"
|
|
87
|
+
if len(parts) <= 1:
|
|
88
|
+
parts, flow = [task], "sequential"
|
|
89
|
+
|
|
90
|
+
tasks, steps = [], []
|
|
91
|
+
for i, p in enumerate(parts, 1):
|
|
92
|
+
r = route(p, registry)
|
|
93
|
+
tid = f"T{i}"
|
|
94
|
+
deps = [f"T{i-1}"] if (i > 1 and flow == "sequential") else []
|
|
95
|
+
tasks.append(PlanTask(id=tid, name=p, suggested_skill=r.primary,
|
|
96
|
+
dependencies=deps))
|
|
97
|
+
steps.append(tid)
|
|
98
|
+
|
|
99
|
+
return Plan(intent=task, tasks=tasks, flow_type=flow, steps=steps)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class StageResult:
|
|
104
|
+
stage: int
|
|
105
|
+
skill_id: str
|
|
106
|
+
result: dict
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class CompositionResult:
|
|
111
|
+
mode: str
|
|
112
|
+
stages: list = field(default_factory=list)
|
|
113
|
+
final_output: dict = field(default_factory=dict)
|
|
114
|
+
failed: list = field(default_factory=list)
|
|
115
|
+
rolled_back: bool = False
|
|
116
|
+
rollback_clean: bool = True
|
|
117
|
+
rollback_log: list = field(default_factory=list)
|
|
118
|
+
needs_manual: list = field(default_factory=list)
|
|
119
|
+
pending_confirmation: Optional[dict] = None
|
|
120
|
+
completed_steps: list = field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def is_pending(self) -> bool:
|
|
124
|
+
"""【P2-2】是否处于待确认暂停态。"""
|
|
125
|
+
return self.pending_confirmation is not None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def is_failed(self) -> bool:
|
|
129
|
+
return bool(self.failed)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_args(skill, task_text: str, upstream: dict) -> dict:
|
|
133
|
+
"""把任务文本映射到 skill 声明的参数。
|
|
134
|
+
|
|
135
|
+
简化策略:单参数 skill → 把任务文本(或提取的算式)塞给那个参数;
|
|
136
|
+
多参数 skill → 交给 skill 自己解析(传 task + upstream)。
|
|
137
|
+
真实系统里这一步由 LLM 做参数抽取;此处做轻量映射保证可运行。
|
|
138
|
+
"""
|
|
139
|
+
params = skill.params or {}
|
|
140
|
+
if not params:
|
|
141
|
+
return {}
|
|
142
|
+
if "expression" in params:
|
|
143
|
+
m = re.search(r"[-+*/().\d\s]+", task_text)
|
|
144
|
+
expr = (m.group(0).strip() if m else task_text)
|
|
145
|
+
return {"expression": expr or task_text}
|
|
146
|
+
if len(params) == 1:
|
|
147
|
+
only = next(iter(params))
|
|
148
|
+
return {only: task_text}
|
|
149
|
+
return {"task": task_text, "upstream_outputs": dict(upstream)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _rollback(executed, registry, res: CompositionResult):
|
|
153
|
+
"""逆序回滚已执行且有副作用的步骤。
|
|
154
|
+
|
|
155
|
+
executed: [(skill_id, output)] 按执行顺序。
|
|
156
|
+
只回滚有副作用(非只读)的;有 compensate 的调用补偿,无的标记需人工处理。
|
|
157
|
+
"""
|
|
158
|
+
res.rolled_back = True
|
|
159
|
+
for sid, output in reversed(executed):
|
|
160
|
+
skill = registry.get(sid)
|
|
161
|
+
if skill is None:
|
|
162
|
+
continue
|
|
163
|
+
side_effect = getattr(skill, "side_effect", "read")
|
|
164
|
+
if side_effect == "read":
|
|
165
|
+
continue
|
|
166
|
+
comp = getattr(skill, "compensate", None)
|
|
167
|
+
if comp is not None:
|
|
168
|
+
try:
|
|
169
|
+
comp(output)
|
|
170
|
+
res.rollback_log.append({"skill": sid, "status": "compensated",
|
|
171
|
+
"detail": "已调用补偿动作撤销副作用"})
|
|
172
|
+
except Exception as e:
|
|
173
|
+
res.rollback_log.append({"skill": sid, "status": "compensate_failed",
|
|
174
|
+
"detail": f"补偿失败: {e}"})
|
|
175
|
+
res.rollback_clean = False
|
|
176
|
+
res.needs_manual.append(sid)
|
|
177
|
+
else:
|
|
178
|
+
res.rollback_log.append({"skill": sid, "status": "manual_required",
|
|
179
|
+
"detail": f"{side_effect} 操作无补偿动作,需人工处理"})
|
|
180
|
+
res.rollback_clean = False
|
|
181
|
+
res.needs_manual.append(sid)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _invoke(adapter, skill, args: dict, confirmed: bool, confirm_nonce: str):
|
|
185
|
+
"""调 `adapter.invoke`,老 adapter 自动降级为 4 参旧签名。
|
|
186
|
+
|
|
187
|
+
测试/第三方 adapter 常只实现 `invoke(skill, args, confirmed=, mode=)`;用签名探测
|
|
188
|
+
而不是 try/except TypeError —— 后者会把 invoke 内部真正的 TypeError 当"签名不兼容"吞掉。
|
|
189
|
+
"""
|
|
190
|
+
fn = adapter.invoke
|
|
191
|
+
try:
|
|
192
|
+
params = inspect.signature(fn).parameters
|
|
193
|
+
accepts = ("confirm_nonce" in params
|
|
194
|
+
or any(p.kind is p.VAR_KEYWORD for p in params.values()))
|
|
195
|
+
except (TypeError, ValueError):
|
|
196
|
+
accepts = False
|
|
197
|
+
if accepts:
|
|
198
|
+
return fn(skill, args, confirmed=confirmed, confirm_nonce=confirm_nonce)
|
|
199
|
+
return fn(skill, args, confirmed=confirmed)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
203
|
+
resume_state: dict | None = None) -> CompositionResult:
|
|
204
|
+
"""skill-composition:按计划顺序执行各 skill。
|
|
205
|
+
|
|
206
|
+
结束态三种:
|
|
207
|
+
· success —— 全部成功
|
|
208
|
+
· pending —— 遇到 needs_confirmation,**暂停(不回滚)**,待用户确认后续跑
|
|
209
|
+
· failed —— 遇到 blocked/circuit_open/error/timeout,逆序回滚补偿
|
|
210
|
+
|
|
211
|
+
【P2-2】needs_confirmation 不再当失败处理:暂停而非回滚,避免撤销已完成的
|
|
212
|
+
有效步骤(反例:创建日程已成功、发邮件待确认,旧逻辑会把日程回滚掉)。
|
|
213
|
+
可选 resume_state(上次 compose 的 pending_confirmation)用于从断点续跑:
|
|
214
|
+
恢复已完成步骤的 upstream/executed 现场,跳过 resume_at 之前的步骤。
|
|
215
|
+
"""
|
|
216
|
+
res = CompositionResult(mode=plan_obj.flow_type)
|
|
217
|
+
upstream = {}
|
|
218
|
+
executed = []
|
|
219
|
+
|
|
220
|
+
resume_at = None
|
|
221
|
+
if resume_state:
|
|
222
|
+
for tid, sid, output in resume_state.get("completed_steps", []):
|
|
223
|
+
upstream[sid] = output
|
|
224
|
+
executed.append((sid, output))
|
|
225
|
+
res.completed_steps.append((tid, sid, output))
|
|
226
|
+
resume_at = resume_state.get("resume_at")
|
|
227
|
+
|
|
228
|
+
for i, t in enumerate(plan_obj.tasks, 1):
|
|
229
|
+
if resume_at:
|
|
230
|
+
if t.id != resume_at:
|
|
231
|
+
continue
|
|
232
|
+
resume_at = None
|
|
233
|
+
sid = t.suggested_skill
|
|
234
|
+
if not sid or not registry.get(sid):
|
|
235
|
+
res.failed.append(t.id); continue
|
|
236
|
+
skill = registry.get(sid)
|
|
237
|
+
call_args = _build_args(skill, t.name, upstream)
|
|
238
|
+
_nonce = (resume_state or {}).get("confirm_nonce", "") or ""
|
|
239
|
+
out = _invoke(adapter, skill, call_args, confirmed, _nonce)
|
|
240
|
+
res.stages.append(StageResult(stage=i, skill_id=sid, result=out.to_dict()))
|
|
241
|
+
if out.success:
|
|
242
|
+
upstream[sid] = out.output
|
|
243
|
+
executed.append((sid, out.output))
|
|
244
|
+
res.completed_steps.append((t.id, sid, out.output))
|
|
245
|
+
elif out.status == "needs_confirmation":
|
|
246
|
+
res.pending_confirmation = {
|
|
247
|
+
"task_id": t.id,
|
|
248
|
+
"skill_id": sid,
|
|
249
|
+
"stage": i,
|
|
250
|
+
"message": out.message,
|
|
251
|
+
"args": call_args,
|
|
252
|
+
"completed_steps": list(res.completed_steps),
|
|
253
|
+
"resume_at": t.id,
|
|
254
|
+
"confirm_nonce": getattr(out, "confirm_nonce", "") or "",
|
|
255
|
+
"confirm_expires_at": getattr(out, "confirm_expires_at", 0.0) or 0.0,
|
|
256
|
+
}
|
|
257
|
+
res.final_output = {
|
|
258
|
+
"pending": True,
|
|
259
|
+
"at": t.id,
|
|
260
|
+
"completed": [s[0] for s in res.completed_steps],
|
|
261
|
+
}
|
|
262
|
+
return res
|
|
263
|
+
else:
|
|
264
|
+
res.failed.append(t.id)
|
|
265
|
+
res.final_output = {"error": out.message, "failed_at": t.id}
|
|
266
|
+
_rollback(executed, registry, res)
|
|
267
|
+
return res
|
|
268
|
+
res.final_output = upstream
|
|
269
|
+
return res
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class PipelineResult:
|
|
274
|
+
task: str
|
|
275
|
+
route: dict
|
|
276
|
+
plan: dict
|
|
277
|
+
composition: dict
|
|
278
|
+
success: bool
|
|
279
|
+
summary: str
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def pending_confirmation(self) -> dict | None:
|
|
283
|
+
"""【P2-2】待确认状态(若无则 None)。结构见 compose 的 pending_confirmation。"""
|
|
284
|
+
return self.composition.get("pending_confirmation")
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def is_pending(self) -> bool:
|
|
288
|
+
"""【P2-2】是否处于待确认暂停态(此时 success=False,但语义是"暂停等确认")。"""
|
|
289
|
+
return self.pending_confirmation is not None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class AHSPipeline:
|
|
293
|
+
"""AHS 执行链编排器:一条龙跑通 router→planner→composition,全程过 guard+breaker+hooks。"""
|
|
294
|
+
|
|
295
|
+
def __init__(self, registry, adapter):
|
|
296
|
+
self.registry = registry
|
|
297
|
+
self.adapter = adapter
|
|
298
|
+
|
|
299
|
+
def run(self, task: str, confirmed: bool = False,
|
|
300
|
+
resume_state: dict | None = None, confirm_nonce: str = "") -> PipelineResult:
|
|
301
|
+
"""同步执行。【P2-2】resume_state 用于从待确认状态断点续跑
|
|
302
|
+
(传上次返回的 pending_confirmation;确认后调 run(task, confirmed=True, resume_state=...))。
|
|
303
|
+
|
|
304
|
+
`confirm_nonce`(关键操作):compose 会把它并进 resume_state 传给 adapter 校验;
|
|
305
|
+
也可以直接放在 resume_state 里(pending_confirmation 已带出该字段)。
|
|
306
|
+
"""
|
|
307
|
+
if confirm_nonce:
|
|
308
|
+
resume_state = {**(resume_state or {}), "confirm_nonce": confirm_nonce}
|
|
309
|
+
self.adapter.hooks.fire("session_start", "", {"task": task})
|
|
310
|
+
|
|
311
|
+
r = route(task, self.registry)
|
|
312
|
+
|
|
313
|
+
p = plan(task, self.registry)
|
|
314
|
+
|
|
315
|
+
c = compose(p, self.registry, self.adapter,
|
|
316
|
+
confirmed=confirmed, resume_state=resume_state)
|
|
317
|
+
|
|
318
|
+
self.adapter.hooks.fire("session_end", "", {"task": task})
|
|
319
|
+
|
|
320
|
+
success = not c.failed and bool(c.stages) and not c.pending_confirmation
|
|
321
|
+
if c.pending_confirmation:
|
|
322
|
+
summary = (f"执行 {len(c.stages)} 步,待确认:"
|
|
323
|
+
f"{c.pending_confirmation['skill_id']}")
|
|
324
|
+
elif c.failed:
|
|
325
|
+
summary = f"执行 {len(c.stages)} 步,部分失败:{','.join(c.failed)}"
|
|
326
|
+
else:
|
|
327
|
+
summary = f"执行 {len(c.stages)} 步,全部成功"
|
|
328
|
+
return PipelineResult(
|
|
329
|
+
task=task,
|
|
330
|
+
route={"primary": r.primary, "confidence": r.confidence,
|
|
331
|
+
"needs_composition": r.needs_composition},
|
|
332
|
+
plan={"intent": p.intent, "steps": p.steps,
|
|
333
|
+
"tasks": [{"id": t.id, "name": t.name, "skill": t.suggested_skill}
|
|
334
|
+
for t in p.tasks]},
|
|
335
|
+
composition={"mode": c.mode, "stages": len(c.stages),
|
|
336
|
+
"failed": c.failed, "final": c.final_output,
|
|
337
|
+
"rolled_back": c.rolled_back, "rollback_clean": c.rollback_clean,
|
|
338
|
+
"rollback_log": c.rollback_log, "needs_manual": c.needs_manual,
|
|
339
|
+
"pending_confirmation": c.pending_confirmation,
|
|
340
|
+
"completed_steps": [s[0] for s in c.completed_steps]},
|
|
341
|
+
success=success, summary=summary)
|
|
342
|
+
|
|
343
|
+
async def run_async(self, task: str, confirmed: bool = False,
|
|
344
|
+
resume_state: dict | None = None,
|
|
345
|
+
confirm_nonce: str = "") -> PipelineResult:
|
|
346
|
+
"""【P2-1】异步入口:把同步 run 放到线程池,避免阻塞事件循环。
|
|
347
|
+
|
|
348
|
+
内部用 asyncio.to_thread 包装,故 run 里的同步 IO(MCP/Web API/本地 handler)
|
|
349
|
+
不会阻塞调用方的事件循环。async 调用方(如 /api/ahs/run)应调本方法。
|
|
350
|
+
注意:asyncio.to_thread 会复制当前 contextvars 到子线程(如 skill actor),
|
|
351
|
+
子线程异常原样抛回 await 处——两者都是想要的行为。
|
|
352
|
+
"""
|
|
353
|
+
return await asyncio.to_thread(self.run, task, confirmed, resume_state,
|
|
354
|
+
confirm_nonce)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""全局技能策略:deny 封杀 + allow 白名单(治理 kill-switch,借鉴 OpenClaw plugins.deny/allow)。
|
|
2
|
+
deny 压倒一切;allow 非空则只放行白名单内。user 层持久化 <data_dir>/skill_policy.json。
|
|
3
|
+
E1 分层:另有 **policy 层** deny(管理员锁定,env FOXCLAW_POLICY_DENY + <data_dir>/policy.json,与
|
|
4
|
+
user 层物理分开),**不可被运行时 undeny 撤销**;经 core/config.py 的 LayeredConfig 合并(policy∪user)。"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SkillPolicy:
|
|
12
|
+
def __init__(self, data_dir):
|
|
13
|
+
self._path = Path(data_dir) / "skill_policy.json"
|
|
14
|
+
self._lock = threading.RLock()
|
|
15
|
+
self._deny: set = set()
|
|
16
|
+
self._allow: set = set()
|
|
17
|
+
from ..core.config import LayeredConfig, load_policy_layer
|
|
18
|
+
policy_layer = load_policy_layer(data_dir)
|
|
19
|
+
self._layers = LayeredConfig(policy=policy_layer, user={"deny": self._deny})
|
|
20
|
+
self._load()
|
|
21
|
+
|
|
22
|
+
def is_blocked(self, skill_id: str) -> bool:
|
|
23
|
+
with self._lock:
|
|
24
|
+
if skill_id in self._layers.merged_set("deny"):
|
|
25
|
+
return True
|
|
26
|
+
if self._allow and skill_id not in self._allow:
|
|
27
|
+
return True
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def deny(self, skill_id: str):
|
|
31
|
+
with self._lock:
|
|
32
|
+
self._deny.add(skill_id)
|
|
33
|
+
self._save()
|
|
34
|
+
|
|
35
|
+
def undeny(self, skill_id: str) -> bool:
|
|
36
|
+
with self._lock:
|
|
37
|
+
if skill_id in self._layers.policy_locked("deny"):
|
|
38
|
+
return False
|
|
39
|
+
existed = skill_id in self._deny
|
|
40
|
+
self._deny.discard(skill_id)
|
|
41
|
+
self._save()
|
|
42
|
+
return existed
|
|
43
|
+
|
|
44
|
+
def set_allow(self, ids):
|
|
45
|
+
with self._lock:
|
|
46
|
+
self._allow = set(ids or [])
|
|
47
|
+
self._save()
|
|
48
|
+
|
|
49
|
+
def snapshot(self) -> dict:
|
|
50
|
+
with self._lock:
|
|
51
|
+
return {"deny": sorted(self._deny), "allow": sorted(self._allow),
|
|
52
|
+
"policy_deny": sorted(self._layers.policy_locked("deny"))}
|
|
53
|
+
|
|
54
|
+
def _load(self):
|
|
55
|
+
if not self._path.exists():
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
d = json.loads(self._path.read_text(encoding="utf-8"))
|
|
59
|
+
self._deny.clear()
|
|
60
|
+
self._deny.update(d.get("deny") or [])
|
|
61
|
+
self._allow = set(d.get("allow") or [])
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def _save(self):
|
|
66
|
+
with self._lock:
|
|
67
|
+
payload = {"deny": sorted(self._deny), "allow": sorted(self._allow)}
|
|
68
|
+
try:
|
|
69
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
tmp = self._path.with_suffix(".tmp")
|
|
71
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
72
|
+
tmp.replace(self._path)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""安装审核门:技能注册前的治理审核(对齐 OpenClaw security.installPolicy)。
|
|
2
|
+
非杀毒——远程 URL 无法扫描;做 SSRF 预检/域名黑名单/明文告警/副作用与信任提示,
|
|
3
|
+
产出 allow|warn|deny,风险项要求用户显式确认。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import os
|
|
6
|
+
import ipaddress
|
|
7
|
+
import socket
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _host_deny() -> set:
|
|
12
|
+
return {h.strip().lower() for h in os.environ.get("FOXCLAW_SKILL_HOST_DENY", "").split(",") if h.strip()}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_internal_host(host: str) -> bool:
|
|
16
|
+
if not host:
|
|
17
|
+
return True
|
|
18
|
+
if host.lower() == "localhost":
|
|
19
|
+
return True
|
|
20
|
+
try:
|
|
21
|
+
infos = socket.getaddrinfo(host, None)
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
for info in infos:
|
|
25
|
+
try:
|
|
26
|
+
addr = ipaddress.ip_address(info[4][0])
|
|
27
|
+
except ValueError:
|
|
28
|
+
continue
|
|
29
|
+
if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
|
|
30
|
+
return True
|
|
31
|
+
if str(addr) == "169.254.169.254":
|
|
32
|
+
return True
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def review_install(spec) -> dict:
|
|
37
|
+
"""返回 {verdict: allow|warn|deny, reasons: [...], warnings: [...]}。"""
|
|
38
|
+
reasons, warnings = [], []
|
|
39
|
+
if spec.kind == "web_api":
|
|
40
|
+
host = (urlparse(spec.endpoint).hostname or "").lower()
|
|
41
|
+
if host in _host_deny():
|
|
42
|
+
reasons.append(f"域名 {host} 在黑名单")
|
|
43
|
+
if not spec.allow_local and _is_internal_host(host):
|
|
44
|
+
reasons.append(f"endpoint 指向内网/本地/元数据地址({host or '?'}),已拦截(SSRF)")
|
|
45
|
+
if spec.endpoint.startswith("http://"):
|
|
46
|
+
warnings.append("使用明文 HTTP,传输不加密")
|
|
47
|
+
if spec.kind == "mcp":
|
|
48
|
+
warnings.append("MCP 来源为外部低信任,其返回内容不应被当作指令执行")
|
|
49
|
+
if spec.side_effect in ("write", "send", "pay") or spec.destructive:
|
|
50
|
+
warnings.append(f"该技能有副作用({spec.side_effect}{'/不可逆' if spec.destructive else ''}),会真实影响外部")
|
|
51
|
+
if reasons:
|
|
52
|
+
return {"verdict": "deny", "reasons": reasons, "warnings": warnings}
|
|
53
|
+
if warnings:
|
|
54
|
+
return {"verdict": "warn", "reasons": [], "warnings": warnings}
|
|
55
|
+
return {"verdict": "allow", "reasons": [], "warnings": []}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def review_mcp_server(url: str) -> dict:
|
|
59
|
+
"""MCP server 接入审核。外部低信任 → 恒 warn(需确认);内网/元数据/黑名单 → deny。"""
|
|
60
|
+
from urllib.parse import urlparse
|
|
61
|
+
reasons, warnings = [], []
|
|
62
|
+
host = (urlparse(url).hostname or "").lower()
|
|
63
|
+
if host in _host_deny():
|
|
64
|
+
reasons.append(f"域名 {host} 在黑名单")
|
|
65
|
+
if _is_internal_host(host):
|
|
66
|
+
reasons.append(f"MCP url 指向内网/本地/元数据地址({host or '?'}),已拦截(SSRF)")
|
|
67
|
+
if url.startswith("http://"):
|
|
68
|
+
warnings.append("使用明文 HTTP,传输不加密")
|
|
69
|
+
warnings.append("接入外部 MCP server,其返回内容为外部不可信数据,不应被当作指令执行")
|
|
70
|
+
if reasons:
|
|
71
|
+
return {"verdict": "deny", "reasons": reasons, "warnings": warnings}
|
|
72
|
+
return {"verdict": "warn", "reasons": [], "warnings": warnings}
|