foxclaw 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -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 +152 -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 +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -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 +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -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 +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -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 +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -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 +148 -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 +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -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 +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -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 +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -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 +1637 -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-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -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 +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -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 +46 -0
- package/python/pyproject.toml +265 -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 +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -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 +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -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,386 @@
|
|
|
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 # 统一规划原语(P1-2:否定/拆分/打分单一真源)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============ skill-router:委托统一引擎,候选空间=skill ============
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class RouteResult:
|
|
28
|
+
primary: Optional[str] = None # 选中的主 skill id
|
|
29
|
+
confidence: float = 0.0
|
|
30
|
+
candidates: list = field(default_factory=list) # [(skill_id, score)]
|
|
31
|
+
fallback: Optional[str] = None
|
|
32
|
+
needs_composition: bool = False
|
|
33
|
+
reason: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def route(task: str, registry, fallback_id: str = None) -> RouteResult:
|
|
37
|
+
"""skill-router:把 skill 适配成通用候选,交统一引擎 trigger_route 打分选择。
|
|
38
|
+
算法(否定过滤触发命中 0.8 + 描述领域匹配 0.2)现由 foxclaw.planning 单一真源提供。"""
|
|
39
|
+
skills = registry.all()
|
|
40
|
+
if not skills:
|
|
41
|
+
return RouteResult(reason="注册表为空")
|
|
42
|
+
cands = [P.ScoredCandidate(
|
|
43
|
+
id=s.id,
|
|
44
|
+
triggers=getattr(s, "triggers", []) or [],
|
|
45
|
+
description=getattr(s, "description", "") or "")
|
|
46
|
+
for s in skills]
|
|
47
|
+
r = P.trigger_route(task, cands, fallback_id=fallback_id)
|
|
48
|
+
return RouteResult(primary=r.primary, confidence=r.confidence,
|
|
49
|
+
candidates=r.candidates, fallback=r.fallback,
|
|
50
|
+
needs_composition=r.needs_composition, reason=r.reason)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============ planner:任务拆解 ============
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class PlanTask:
|
|
57
|
+
id: str
|
|
58
|
+
name: str
|
|
59
|
+
suggested_skill: Optional[str] = None
|
|
60
|
+
dependencies: list = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Plan:
|
|
65
|
+
intent: str
|
|
66
|
+
tasks: list = field(default_factory=list) # [PlanTask]
|
|
67
|
+
flow_type: str = "sequential" # sequential/parallel/hybrid
|
|
68
|
+
steps: list = field(default_factory=list) # 执行顺序 [task_id]
|
|
69
|
+
needs_input: bool = False
|
|
70
|
+
missing: list = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def plan(task: str, registry) -> Plan:
|
|
74
|
+
"""planner:把任务拆成结构化计划。拆分/并行标记与算法由 foxclaw.planning 单一真源提供。
|
|
75
|
+
|
|
76
|
+
- 含并行连接词(同时/分别/并行)→ 拆成**无依赖**子任务,flow_type=parallel;
|
|
77
|
+
- 含顺序连接词(然后/接着…)→ 拆成**线性依赖链**,flow_type=sequential;
|
|
78
|
+
- 都没有 → 单步。
|
|
79
|
+
依赖关系决定能否并行:parallel 的子任务 dependencies 为空,sequential 的链式依赖前一步。
|
|
80
|
+
"""
|
|
81
|
+
task = (task or "").strip()
|
|
82
|
+
if not task:
|
|
83
|
+
return Plan(intent="", needs_input=True, missing=["task"])
|
|
84
|
+
|
|
85
|
+
if any(k in task for k in P.PARALLEL_TRIGGERS):
|
|
86
|
+
parts = P.split_on(task, P.PARALLEL_TRIGGERS + P.LIST_SEPS)
|
|
87
|
+
flow = "parallel" if len(parts) > 1 else "sequential"
|
|
88
|
+
else:
|
|
89
|
+
parts = P.split_on(task, P.SPLIT_MARKERS)
|
|
90
|
+
flow = "sequential"
|
|
91
|
+
if len(parts) <= 1:
|
|
92
|
+
parts, flow = [task], "sequential"
|
|
93
|
+
|
|
94
|
+
tasks, steps = [], []
|
|
95
|
+
for i, p in enumerate(parts, 1):
|
|
96
|
+
r = route(p, registry)
|
|
97
|
+
tid = f"T{i}"
|
|
98
|
+
# 顺序:依赖前一步;并行:无依赖(可同时 fork)
|
|
99
|
+
deps = [f"T{i-1}"] if (i > 1 and flow == "sequential") else []
|
|
100
|
+
tasks.append(PlanTask(id=tid, name=p, suggested_skill=r.primary,
|
|
101
|
+
dependencies=deps))
|
|
102
|
+
steps.append(tid)
|
|
103
|
+
|
|
104
|
+
return Plan(intent=task, tasks=tasks, flow_type=flow, steps=steps)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ============ composition:多 skill 编排执行 ============
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class StageResult:
|
|
111
|
+
stage: int
|
|
112
|
+
skill_id: str
|
|
113
|
+
result: dict
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class CompositionResult:
|
|
118
|
+
mode: str
|
|
119
|
+
stages: list = field(default_factory=list) # [StageResult]
|
|
120
|
+
final_output: dict = field(default_factory=dict)
|
|
121
|
+
failed: list = field(default_factory=list)
|
|
122
|
+
rolled_back: bool = False
|
|
123
|
+
rollback_clean: bool = True # 回滚是否干净完成(无补偿失败/无需人工)
|
|
124
|
+
rollback_log: list = field(default_factory=list) # 回滚记录 [{skill,status,detail}]
|
|
125
|
+
needs_manual: list = field(default_factory=list) # 需人工处理的 skill(无补偿/补偿失败)
|
|
126
|
+
# 【P2-2】待确认状态:遇到 needs_confirmation 时填充(不回滚),供调用方展示+断点续跑
|
|
127
|
+
pending_confirmation: dict = None # {task_id,skill_id,stage,message,args,completed_steps,resume_at}
|
|
128
|
+
# 【P2-2】已完成步骤(断点续跑所需的现场): [(task_id, skill_id, output)]
|
|
129
|
+
completed_steps: list = field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_pending(self) -> bool:
|
|
133
|
+
"""【P2-2】是否处于待确认暂停态。"""
|
|
134
|
+
return self.pending_confirmation is not None
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def is_failed(self) -> bool:
|
|
138
|
+
return bool(self.failed)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_args(skill, task_text: str, upstream: dict) -> dict:
|
|
142
|
+
"""把任务文本映射到 skill 声明的参数。
|
|
143
|
+
|
|
144
|
+
简化策略:单参数 skill → 把任务文本(或提取的算式)塞给那个参数;
|
|
145
|
+
多参数 skill → 交给 skill 自己解析(传 task + upstream)。
|
|
146
|
+
真实系统里这一步由 LLM 做参数抽取;此处做轻量映射保证可运行。
|
|
147
|
+
"""
|
|
148
|
+
params = skill.params or {}
|
|
149
|
+
if not params:
|
|
150
|
+
return {}
|
|
151
|
+
# 计算器类:从文本提取数学表达式
|
|
152
|
+
if "expression" in params:
|
|
153
|
+
m = re.search(r"[-+*/().\d\s]+", task_text)
|
|
154
|
+
expr = (m.group(0).strip() if m else task_text)
|
|
155
|
+
return {"expression": expr or task_text}
|
|
156
|
+
# 单参数 skill:把整段任务文本作为该参数
|
|
157
|
+
if len(params) == 1:
|
|
158
|
+
only = next(iter(params))
|
|
159
|
+
return {only: task_text}
|
|
160
|
+
# 多参数:传 task,让下游(或 LLM)解析;附带上游
|
|
161
|
+
return {"task": task_text, "upstream_outputs": dict(upstream)}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _rollback(executed, registry, res: CompositionResult):
|
|
165
|
+
"""逆序回滚已执行且有副作用的步骤。
|
|
166
|
+
|
|
167
|
+
executed: [(skill_id, output)] 按执行顺序。
|
|
168
|
+
只回滚有副作用(非只读)的;有 compensate 的调用补偿,无的标记需人工处理。
|
|
169
|
+
"""
|
|
170
|
+
res.rolled_back = True
|
|
171
|
+
for sid, output in reversed(executed):
|
|
172
|
+
skill = registry.get(sid)
|
|
173
|
+
if skill is None:
|
|
174
|
+
continue
|
|
175
|
+
side_effect = getattr(skill, "side_effect", "read")
|
|
176
|
+
if side_effect == "read":
|
|
177
|
+
continue # 只读操作无副作用,无需回滚
|
|
178
|
+
comp = getattr(skill, "compensate", None)
|
|
179
|
+
if comp is not None:
|
|
180
|
+
try:
|
|
181
|
+
comp(output)
|
|
182
|
+
res.rollback_log.append({"skill": sid, "status": "compensated",
|
|
183
|
+
"detail": "已调用补偿动作撤销副作用"})
|
|
184
|
+
except Exception as e:
|
|
185
|
+
res.rollback_log.append({"skill": sid, "status": "compensate_failed",
|
|
186
|
+
"detail": f"补偿失败: {e}"})
|
|
187
|
+
res.rollback_clean = False # 补偿失败 → 回滚不干净
|
|
188
|
+
res.needs_manual.append(sid)
|
|
189
|
+
else:
|
|
190
|
+
res.rollback_log.append({"skill": sid, "status": "manual_required",
|
|
191
|
+
"detail": f"{side_effect} 操作无补偿动作,需人工处理"})
|
|
192
|
+
res.rollback_clean = False # 有副作用却无法补偿 → 不干净
|
|
193
|
+
res.needs_manual.append(sid)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _invoke(adapter, skill, args: dict, confirmed: bool, confirm_nonce: str):
|
|
197
|
+
"""调 `adapter.invoke`,老 adapter 自动降级为 4 参旧签名。
|
|
198
|
+
|
|
199
|
+
测试/第三方 adapter 常只实现 `invoke(skill, args, confirmed=, mode=)`;用签名探测
|
|
200
|
+
而不是 try/except TypeError —— 后者会把 invoke 内部真正的 TypeError 当"签名不兼容"吞掉。
|
|
201
|
+
"""
|
|
202
|
+
fn = adapter.invoke
|
|
203
|
+
try:
|
|
204
|
+
params = inspect.signature(fn).parameters
|
|
205
|
+
accepts = ("confirm_nonce" in params
|
|
206
|
+
or any(p.kind is p.VAR_KEYWORD for p in params.values()))
|
|
207
|
+
except (TypeError, ValueError):
|
|
208
|
+
accepts = False
|
|
209
|
+
if accepts:
|
|
210
|
+
return fn(skill, args, confirmed=confirmed, confirm_nonce=confirm_nonce)
|
|
211
|
+
return fn(skill, args, confirmed=confirmed)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def compose(plan_obj: Plan, registry, adapter, confirmed: bool = False,
|
|
215
|
+
resume_state: dict = None) -> CompositionResult:
|
|
216
|
+
"""skill-composition:按计划顺序执行各 skill。
|
|
217
|
+
|
|
218
|
+
结束态三种:
|
|
219
|
+
· success —— 全部成功
|
|
220
|
+
· pending —— 遇到 needs_confirmation,**暂停(不回滚)**,待用户确认后续跑
|
|
221
|
+
· failed —— 遇到 blocked/circuit_open/error/timeout,逆序回滚补偿
|
|
222
|
+
|
|
223
|
+
【P2-2】needs_confirmation 不再当失败处理:暂停而非回滚,避免撤销已完成的
|
|
224
|
+
有效步骤(反例:创建日程已成功、发邮件待确认,旧逻辑会把日程回滚掉)。
|
|
225
|
+
可选 resume_state(上次 compose 的 pending_confirmation)用于从断点续跑:
|
|
226
|
+
恢复已完成步骤的 upstream/executed 现场,跳过 resume_at 之前的步骤。
|
|
227
|
+
"""
|
|
228
|
+
res = CompositionResult(mode=plan_obj.flow_type)
|
|
229
|
+
upstream = {} # skill_id -> output(供下游读取)
|
|
230
|
+
executed = [] # [(skill_id, output)] 按执行顺序,供回滚
|
|
231
|
+
|
|
232
|
+
# 【P2-2】恢复已完成步骤(断点续跑)
|
|
233
|
+
resume_at = None
|
|
234
|
+
if resume_state:
|
|
235
|
+
for tid, sid, output in resume_state.get("completed_steps", []):
|
|
236
|
+
upstream[sid] = output
|
|
237
|
+
executed.append((sid, output))
|
|
238
|
+
res.completed_steps.append((tid, sid, output))
|
|
239
|
+
resume_at = resume_state.get("resume_at")
|
|
240
|
+
|
|
241
|
+
for i, t in enumerate(plan_obj.tasks, 1):
|
|
242
|
+
# 【P2-2】跳过已完成的步骤(断点续跑):resume_at 之前的步骤不重跑;
|
|
243
|
+
# 命中断点后清空 resume_at,让断点及其之后的步骤正常执行。
|
|
244
|
+
# (若照抄"!= resume_at 就 continue",断点之后的步骤会被一并跳过)
|
|
245
|
+
if resume_at:
|
|
246
|
+
if t.id != resume_at:
|
|
247
|
+
continue
|
|
248
|
+
resume_at = None
|
|
249
|
+
sid = t.suggested_skill
|
|
250
|
+
if not sid or not registry.get(sid):
|
|
251
|
+
res.failed.append(t.id); continue
|
|
252
|
+
skill = registry.get(sid)
|
|
253
|
+
call_args = _build_args(skill, t.name, upstream)
|
|
254
|
+
# 关键操作二次确认码随 resume_state 回灌:同一 nonce 只能在原动作+原参数上用一次
|
|
255
|
+
_nonce = (resume_state or {}).get("confirm_nonce", "") or ""
|
|
256
|
+
out = _invoke(adapter, skill, call_args, confirmed, _nonce)
|
|
257
|
+
res.stages.append(StageResult(stage=i, skill_id=sid, result=out.to_dict()))
|
|
258
|
+
if out.success:
|
|
259
|
+
upstream[sid] = out.output
|
|
260
|
+
executed.append((sid, out.output))
|
|
261
|
+
res.completed_steps.append((t.id, sid, out.output))
|
|
262
|
+
elif out.status == "needs_confirmation":
|
|
263
|
+
# 【P2-2】待确认 → 暂停,不回滚;把断点现场打包给调用方
|
|
264
|
+
res.pending_confirmation = {
|
|
265
|
+
"task_id": t.id,
|
|
266
|
+
"skill_id": sid,
|
|
267
|
+
"stage": i,
|
|
268
|
+
"message": out.message,
|
|
269
|
+
"args": call_args,
|
|
270
|
+
"completed_steps": list(res.completed_steps),
|
|
271
|
+
"resume_at": t.id,
|
|
272
|
+
# 关键操作:一次性确认码(绑定 skill + 本次 args,过期即废)。
|
|
273
|
+
# 续跑时把它塞回 resume_state 一起传,才能通过 adapter 的二次确认校验。
|
|
274
|
+
"confirm_nonce": getattr(out, "confirm_nonce", "") or "",
|
|
275
|
+
"confirm_expires_at": getattr(out, "confirm_expires_at", 0.0) or 0.0,
|
|
276
|
+
}
|
|
277
|
+
res.final_output = {
|
|
278
|
+
"pending": True,
|
|
279
|
+
"at": t.id,
|
|
280
|
+
"completed": [s[0] for s in res.completed_steps],
|
|
281
|
+
}
|
|
282
|
+
return res
|
|
283
|
+
else:
|
|
284
|
+
# blocked/circuit_open/error/timeout → 关键路径失败 → 逆序回滚
|
|
285
|
+
res.failed.append(t.id)
|
|
286
|
+
res.final_output = {"error": out.message, "failed_at": t.id}
|
|
287
|
+
_rollback(executed, registry, res)
|
|
288
|
+
return res
|
|
289
|
+
res.final_output = upstream
|
|
290
|
+
return res
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ============ 编排器:把 6 个元 skill 串起来 ============
|
|
294
|
+
|
|
295
|
+
@dataclass
|
|
296
|
+
class PipelineResult:
|
|
297
|
+
task: str
|
|
298
|
+
route: dict
|
|
299
|
+
plan: dict
|
|
300
|
+
composition: dict
|
|
301
|
+
success: bool
|
|
302
|
+
summary: str
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def pending_confirmation(self) -> dict:
|
|
306
|
+
"""【P2-2】待确认状态(若无则 None)。结构见 compose 的 pending_confirmation。"""
|
|
307
|
+
return self.composition.get("pending_confirmation")
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def is_pending(self) -> bool:
|
|
311
|
+
"""【P2-2】是否处于待确认暂停态(此时 success=False,但语义是"暂停等确认")。"""
|
|
312
|
+
return self.pending_confirmation is not None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class AHSPipeline:
|
|
316
|
+
"""AHS 执行链编排器:一条龙跑通 router→planner→composition,全程过 guard+breaker+hooks。"""
|
|
317
|
+
|
|
318
|
+
def __init__(self, registry, adapter):
|
|
319
|
+
self.registry = registry
|
|
320
|
+
self.adapter = adapter
|
|
321
|
+
|
|
322
|
+
def run(self, task: str, confirmed: bool = False,
|
|
323
|
+
resume_state: dict = None, confirm_nonce: str = "") -> PipelineResult:
|
|
324
|
+
"""同步执行。【P2-2】resume_state 用于从待确认状态断点续跑
|
|
325
|
+
(传上次返回的 pending_confirmation;确认后调 run(task, confirmed=True, resume_state=...))。
|
|
326
|
+
|
|
327
|
+
`confirm_nonce`(关键操作):compose 会把它并进 resume_state 传给 adapter 校验;
|
|
328
|
+
也可以直接放在 resume_state 里(pending_confirmation 已带出该字段)。
|
|
329
|
+
"""
|
|
330
|
+
if confirm_nonce:
|
|
331
|
+
resume_state = {**(resume_state or {}), "confirm_nonce": confirm_nonce}
|
|
332
|
+
# session_start 钩子
|
|
333
|
+
self.adapter.hooks.fire("session_start", "", {"task": task})
|
|
334
|
+
|
|
335
|
+
# 1. router 选主 skill
|
|
336
|
+
r = route(task, self.registry)
|
|
337
|
+
|
|
338
|
+
# 2. planner 拆解(总是出计划;简单任务=单步)
|
|
339
|
+
p = plan(task, self.registry)
|
|
340
|
+
|
|
341
|
+
# 3. composition 编排执行(每步内部过 guard+breaker+hooks)
|
|
342
|
+
c = compose(p, self.registry, self.adapter,
|
|
343
|
+
confirmed=confirmed, resume_state=resume_state)
|
|
344
|
+
|
|
345
|
+
# session_end 钩子
|
|
346
|
+
self.adapter.hooks.fire("session_end", "", {"task": task})
|
|
347
|
+
|
|
348
|
+
# 【P2-2】success 判定区分 pending:待确认不是失败,但也不是成功
|
|
349
|
+
# (旧语义:pending 会进 failed → success=False;新语义:pending 非空 → success=False。
|
|
350
|
+
# 外向结果一致,语义不同——pending 的步骤没有被回滚,可以续跑)
|
|
351
|
+
success = not c.failed and bool(c.stages) and not c.pending_confirmation
|
|
352
|
+
if c.pending_confirmation:
|
|
353
|
+
summary = (f"执行 {len(c.stages)} 步,待确认:"
|
|
354
|
+
f"{c.pending_confirmation['skill_id']}")
|
|
355
|
+
elif c.failed:
|
|
356
|
+
summary = f"执行 {len(c.stages)} 步,部分失败:{','.join(c.failed)}"
|
|
357
|
+
else:
|
|
358
|
+
summary = f"执行 {len(c.stages)} 步,全部成功"
|
|
359
|
+
return PipelineResult(
|
|
360
|
+
task=task,
|
|
361
|
+
route={"primary": r.primary, "confidence": r.confidence,
|
|
362
|
+
"needs_composition": r.needs_composition},
|
|
363
|
+
plan={"intent": p.intent, "steps": p.steps,
|
|
364
|
+
"tasks": [{"id": t.id, "name": t.name, "skill": t.suggested_skill}
|
|
365
|
+
for t in p.tasks]},
|
|
366
|
+
composition={"mode": c.mode, "stages": len(c.stages),
|
|
367
|
+
"failed": c.failed, "final": c.final_output,
|
|
368
|
+
"rolled_back": c.rolled_back, "rollback_clean": c.rollback_clean,
|
|
369
|
+
"rollback_log": c.rollback_log, "needs_manual": c.needs_manual,
|
|
370
|
+
# 【P2-2】待确认 + 已完成步骤(供前端识别/续跑)
|
|
371
|
+
"pending_confirmation": c.pending_confirmation,
|
|
372
|
+
"completed_steps": [s[0] for s in c.completed_steps]},
|
|
373
|
+
success=success, summary=summary)
|
|
374
|
+
|
|
375
|
+
async def run_async(self, task: str, confirmed: bool = False,
|
|
376
|
+
resume_state: dict = None,
|
|
377
|
+
confirm_nonce: str = "") -> PipelineResult:
|
|
378
|
+
"""【P2-1】异步入口:把同步 run 放到线程池,避免阻塞事件循环。
|
|
379
|
+
|
|
380
|
+
内部用 asyncio.to_thread 包装,故 run 里的同步 IO(MCP/Web API/本地 handler)
|
|
381
|
+
不会阻塞调用方的事件循环。async 调用方(如 /api/ahs/run)应调本方法。
|
|
382
|
+
注意:asyncio.to_thread 会复制当前 contextvars 到子线程(如 skill actor),
|
|
383
|
+
子线程异常原样抛回 await 处——两者都是想要的行为。
|
|
384
|
+
"""
|
|
385
|
+
return await asyncio.to_thread(self.run, task, confirmed, resume_state,
|
|
386
|
+
confirm_nonce)
|
|
@@ -0,0 +1,80 @@
|
|
|
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() # user 层:运行时可 deny/undeny,持久化到 skill_policy.json
|
|
16
|
+
self._allow: set = set() # 空 = 全放行
|
|
17
|
+
# E1 分层:policy 层 deny(管理员锁定,env FOXCLAW_POLICY_DENY + <data_dir>/policy.json)。
|
|
18
|
+
# 与 user 层物理分开、不写回 skill_policy.json;**不可被 undeny 撤销**。默认空 = 现状。
|
|
19
|
+
from ..core.config import LayeredConfig, load_policy_layer
|
|
20
|
+
policy_layer = load_policy_layer(data_dir)
|
|
21
|
+
# user 层传 self._deny(set 引用)→ 运行时增删被 merged_set 实时反映,无需重建 LayeredConfig
|
|
22
|
+
self._layers = LayeredConfig(policy=policy_layer, user={"deny": self._deny})
|
|
23
|
+
self._load()
|
|
24
|
+
|
|
25
|
+
def is_blocked(self, skill_id: str) -> bool:
|
|
26
|
+
with self._lock:
|
|
27
|
+
# deny = policy 层 ∪ user 层(只增不减);deny 压倒 allow
|
|
28
|
+
if skill_id in self._layers.merged_set("deny"):
|
|
29
|
+
return True
|
|
30
|
+
if self._allow and skill_id not in self._allow:
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def deny(self, skill_id: str):
|
|
35
|
+
with self._lock:
|
|
36
|
+
self._deny.add(skill_id)
|
|
37
|
+
self._save()
|
|
38
|
+
|
|
39
|
+
def undeny(self, skill_id: str) -> bool:
|
|
40
|
+
with self._lock:
|
|
41
|
+
if skill_id in self._layers.policy_locked("deny"):
|
|
42
|
+
return False # E1:policy 层为管理员锁定,用户 undeny 无效(deny 铁律)
|
|
43
|
+
existed = skill_id in self._deny
|
|
44
|
+
self._deny.discard(skill_id)
|
|
45
|
+
self._save()
|
|
46
|
+
return existed
|
|
47
|
+
|
|
48
|
+
def set_allow(self, ids):
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._allow = set(ids or [])
|
|
51
|
+
self._save()
|
|
52
|
+
|
|
53
|
+
def snapshot(self) -> dict:
|
|
54
|
+
with self._lock:
|
|
55
|
+
return {"deny": sorted(self._deny), "allow": sorted(self._allow),
|
|
56
|
+
"policy_deny": sorted(self._layers.policy_locked("deny"))} # E1:管理员锁定项(只读)
|
|
57
|
+
|
|
58
|
+
def _load(self):
|
|
59
|
+
if not self._path.exists():
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
d = json.loads(self._path.read_text(encoding="utf-8"))
|
|
63
|
+
# 就地更新 self._deny(不重新赋值!)——LayeredConfig.user 持有本 set 的引用,
|
|
64
|
+
# 重新赋值会断开引用,导致从盘加载的 user deny 不被 merged_set 反映(E1 集成的坑)。
|
|
65
|
+
self._deny.clear()
|
|
66
|
+
self._deny.update(d.get("deny") or [])
|
|
67
|
+
self._allow = set(d.get("allow") or [])
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def _save(self):
|
|
72
|
+
with self._lock:
|
|
73
|
+
payload = {"deny": sorted(self._deny), "allow": sorted(self._allow)}
|
|
74
|
+
try:
|
|
75
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
tmp = self._path.with_suffix(".tmp")
|
|
77
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
78
|
+
tmp.replace(self._path)
|
|
79
|
+
except Exception:
|
|
80
|
+
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 # 解析不了 → 不在安装期硬挡,交运行时 SSRF 兜底
|
|
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} # 外部低信任恒 warn
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""用户安装技能 + 内置技能启停覆盖的持久化。知识/记忆各有真源;技能注册表原为纯内存,
|
|
2
|
+
本层让"安装"重启不丢。文件:<data_dir>/user_skills.json。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dataclasses import fields as _fields
|
|
8
|
+
|
|
9
|
+
from . import SkillSpec, SkillRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SkillStore:
|
|
13
|
+
def __init__(self, data_dir):
|
|
14
|
+
self._path = Path(data_dir) / "user_skills.json"
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def _spec_fields(self) -> set:
|
|
18
|
+
return {f.name for f in _fields(SkillSpec) if f.name != "handler"}
|
|
19
|
+
|
|
20
|
+
def load(self, registry: SkillRegistry):
|
|
21
|
+
"""启动恢复:注册用户技能 + 应用内置启停覆盖。文件缺失/损坏 → 静默跳过(全新库)。"""
|
|
22
|
+
if not self._path.exists():
|
|
23
|
+
return
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
26
|
+
except Exception:
|
|
27
|
+
return
|
|
28
|
+
allowed = self._spec_fields()
|
|
29
|
+
for d in data.get("skills", []):
|
|
30
|
+
try:
|
|
31
|
+
spec = SkillSpec(**{k: v for k, v in d.items() if k in allowed})
|
|
32
|
+
spec.source = "user"
|
|
33
|
+
registry.register(spec)
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
for sid, ov in (data.get("overrides") or {}).items():
|
|
37
|
+
sp = registry.get(sid)
|
|
38
|
+
if sp is not None and "enabled" in ov:
|
|
39
|
+
sp.enabled = bool(ov["enabled"])
|
|
40
|
+
|
|
41
|
+
def save(self, registry: SkillRegistry):
|
|
42
|
+
"""把用户技能 + 被改过 enabled 的内置技能原子写回。写盘失败不抛(内存注册仍生效)。"""
|
|
43
|
+
with self._lock:
|
|
44
|
+
user_skills, overrides = [], {}
|
|
45
|
+
for sp in registry.all_including_disabled():
|
|
46
|
+
if sp.source == "user":
|
|
47
|
+
user_skills.append(sp.to_dict())
|
|
48
|
+
elif sp.enabled is False:
|
|
49
|
+
overrides[sp.id] = {"enabled": False}
|
|
50
|
+
payload = {"skills": user_skills, "overrides": overrides}
|
|
51
|
+
try:
|
|
52
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
tmp = self._path.with_suffix(".tmp")
|
|
54
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
55
|
+
tmp.replace(self._path)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* 鉴权注入:后端若设了 FOXCLAW_API_KEY,同源 /api、/knowledge 请求需带 X-API-Key。
|
|
2
|
+
从 localStorage.foxclawApiKey 读取并包一层 fetch 自动注入。
|
|
3
|
+
必须最先加载(在 settings.js 等业务模块之前),否则模块顶层提前发出的请求
|
|
4
|
+
(如 settings.js 的 loadModel → /api/model)会漏带 key 被 401。 */
|
|
5
|
+
(function(){
|
|
6
|
+
if(window.__foxclawFetchPatched) return;
|
|
7
|
+
const _fetch=window.fetch.bind(window);
|
|
8
|
+
let _authHinted=false;
|
|
9
|
+
window.fetch=function(url,opts){
|
|
10
|
+
try{
|
|
11
|
+
const key=localStorage.getItem('foxclawApiKey');
|
|
12
|
+
const u=String(url||'');
|
|
13
|
+
// /knowledge/* 与 /api/* 一样受后端可选鉴权守护,两者都要注入 key
|
|
14
|
+
// /stats 是 HKC 统计数据面,后端已纳入守护,同样需要注入
|
|
15
|
+
if(key && (u.indexOf('/api')>=0 || u.indexOf('/knowledge')>=0 || u.indexOf('/stats')>=0)){
|
|
16
|
+
opts=opts||{}; opts.headers=Object.assign({}, opts.headers, {'X-API-Key':key});
|
|
17
|
+
}
|
|
18
|
+
}catch(e){}
|
|
19
|
+
const p=_fetch(url,opts);
|
|
20
|
+
// 401 检测:后端开了鉴权但本机浏览器没配 key(或 key 错)→ 弹提示条引导(只弹一次)
|
|
21
|
+
try{
|
|
22
|
+
p.then(r=>{
|
|
23
|
+
if(r && r.status===401 && !_authHinted){
|
|
24
|
+
_authHinted=true;
|
|
25
|
+
if(typeof window.__foxclawAuthHint==='function') window.__foxclawAuthHint();
|
|
26
|
+
}
|
|
27
|
+
}).catch(()=>{});
|
|
28
|
+
}catch(e){}
|
|
29
|
+
return p;
|
|
30
|
+
};
|
|
31
|
+
window.__foxclawFetchPatched=true;
|
|
32
|
+
})();
|