foxclaw 0.1.2 → 0.1.4
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/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- 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 +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -29,7 +29,7 @@ def _score(query: str, text: str) -> int:
|
|
|
29
29
|
for term in re.split(r"[\s,,、;;::/]+", q):
|
|
30
30
|
if len(term) >= 2 and term in t:
|
|
31
31
|
score += 2
|
|
32
|
-
for i in range(len(q) - 1):
|
|
32
|
+
for i in range(len(q) - 1):
|
|
33
33
|
if q[i:i + 2] in t:
|
|
34
34
|
score += 1
|
|
35
35
|
return score
|
|
@@ -65,13 +65,12 @@ class AgentSourcePort:
|
|
|
65
65
|
try:
|
|
66
66
|
scored += gather(text, top_k)
|
|
67
67
|
except Exception:
|
|
68
|
-
continue
|
|
68
|
+
continue
|
|
69
69
|
scored.sort(key=lambda x: x[0], reverse=True)
|
|
70
70
|
return [ev for _, ev in scored[:max(top_k * 2, top_k)]]
|
|
71
71
|
|
|
72
|
-
# ── 1. 领域内核 ──────────────────────────────────────────
|
|
73
72
|
def _from_finance(self, text, top_k):
|
|
74
|
-
out = []
|
|
73
|
+
out: list[tuple[float, RawEvidence]] = []
|
|
75
74
|
rt = self.finance
|
|
76
75
|
if rt is None:
|
|
77
76
|
return out
|
|
@@ -101,7 +100,7 @@ class AgentSourcePort:
|
|
|
101
100
|
return out
|
|
102
101
|
|
|
103
102
|
def _from_learning(self, text, top_k):
|
|
104
|
-
out = []
|
|
103
|
+
out: list[tuple[float, RawEvidence]] = []
|
|
105
104
|
rt = self.learning
|
|
106
105
|
if rt is None:
|
|
107
106
|
return out
|
|
@@ -116,7 +115,7 @@ class AgentSourcePort:
|
|
|
116
115
|
return out
|
|
117
116
|
|
|
118
117
|
def _from_itinerary(self, text, top_k):
|
|
119
|
-
out = []
|
|
118
|
+
out: list[tuple[float, RawEvidence]] = []
|
|
120
119
|
rt = self.itinerary
|
|
121
120
|
if rt is None:
|
|
122
121
|
return out
|
|
@@ -130,9 +129,8 @@ class AgentSourcePort:
|
|
|
130
129
|
confidence=_conf(s))))
|
|
131
130
|
return out
|
|
132
131
|
|
|
133
|
-
# ── 2. ThoughtChain 推理链 ───────────────────────────────
|
|
134
132
|
def _from_thoughtchain(self, text, top_k):
|
|
135
|
-
out = []
|
|
133
|
+
out: list[tuple[float, RawEvidence]] = []
|
|
136
134
|
tc = self.thought_chain
|
|
137
135
|
if tc is None or not hasattr(tc, "search_nodes"):
|
|
138
136
|
return out
|
|
@@ -145,9 +143,8 @@ class AgentSourcePort:
|
|
|
145
143
|
confidence=_conf(max(s, 1)))))
|
|
146
144
|
return out
|
|
147
145
|
|
|
148
|
-
# ── 3. 各 Agent 的 HMR 记忆 ─────────────────────────────
|
|
149
146
|
def _from_memories(self, text, top_k):
|
|
150
|
-
out = []
|
|
147
|
+
out: list[tuple[float, RawEvidence]] = []
|
|
151
148
|
if self.hmr is None:
|
|
152
149
|
return out
|
|
153
150
|
for aid in (self.agents.keys() if self.agents else []):
|
|
@@ -167,7 +164,6 @@ class AgentSourcePort:
|
|
|
167
164
|
confidence=_conf(s))))
|
|
168
165
|
return out
|
|
169
166
|
|
|
170
|
-
# ── 4. 能力/状态目录 ─────────────────────────────────────
|
|
171
167
|
def _from_capabilities(self, text, top_k):
|
|
172
168
|
out = []
|
|
173
169
|
reg = self.skill_registry
|
|
@@ -24,7 +24,6 @@ from .models import RawEvidence, EvidenceGraph, EvidenceNode, EvidenceEdge
|
|
|
24
24
|
_SIMILARITY_SUPPORT = 0.6
|
|
25
25
|
_SIMILARITY_DUP = 0.92
|
|
26
26
|
|
|
27
|
-
# 否定词:ASCII 按词边界(防 not⊂another/notebook、no⊂know);中文按子串(中文无词边界)
|
|
28
27
|
_NEG_ASCII = ("not", "no longer", "never", "cannot", "isn't", "aren't", "won't")
|
|
29
28
|
_NEG_CJK = ("不是", "并非", "并不", "没有", "无法", "不会", "未能")
|
|
30
29
|
_NEG_ASCII_RE = re.compile(
|
|
@@ -37,9 +36,6 @@ def _has_neg(text: str) -> bool:
|
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
def _sem_model_name() -> str:
|
|
40
|
-
# 【P2·单 embedding 入口】解析链:与 harness.py 一致 ——
|
|
41
|
-
# 显式传参 > EMBEDDING_MODEL(单入口) > HKC_EMBEDDING_MODEL > FOXCLAW_EVIDENCE_MODEL > HKC_KEE_SEMANTIC_MODEL > MiniLM 兜底。
|
|
42
|
-
# 与 KEE 默认同名 → get_st_model 共享同一实例;可经环境变量换任意 ST 模型(模型无关)
|
|
43
39
|
return (os.environ.get("FOXCLAW_EVIDENCE_MODEL")
|
|
44
40
|
or os.environ.get("EMBEDDING_MODEL")
|
|
45
41
|
or os.environ.get("HKC_EMBEDDING_MODEL")
|
|
@@ -23,9 +23,9 @@ _UNGROUNDED_PENALTY = 0.15
|
|
|
23
23
|
|
|
24
24
|
class InternalSearchEngine:
|
|
25
25
|
def __init__(self, memory_port=None, knowledge_port=None, agent_port=None):
|
|
26
|
-
self.memory_port = memory_port
|
|
27
|
-
self.knowledge_port = knowledge_port
|
|
28
|
-
self.agent_port = agent_port
|
|
26
|
+
self.memory_port = memory_port
|
|
27
|
+
self.knowledge_port = knowledge_port
|
|
28
|
+
self.agent_port = agent_port
|
|
29
29
|
|
|
30
30
|
@staticmethod
|
|
31
31
|
def _safe_query(port, text: str, top_k: int) -> list[RawEvidence]:
|
|
@@ -46,8 +46,6 @@ class InternalSearchEngine:
|
|
|
46
46
|
memory_hits = self._safe_query(self.memory_port, text, top_k)
|
|
47
47
|
agent_hits = self._safe_query(self.agent_port, text, top_k)
|
|
48
48
|
|
|
49
|
-
# 知识候选用「内部一切可溯源证据」(记忆 + Agent 态势)交叉验证:找得到就 grounded,
|
|
50
|
-
# 找不到就做孤证惩罚。这样 HKC 权威知识与 Agent 实际持有的数据互为印证。
|
|
51
49
|
if knowledge_hits:
|
|
52
50
|
knowledge_hits = self._cross_link(knowledge_hits, memory_hits + agent_hits)
|
|
53
51
|
|
|
@@ -12,17 +12,16 @@ from typing import Any, Optional
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class RouteType(str, Enum):
|
|
15
|
-
INTERNAL = "HSR-I"
|
|
16
|
-
EXTERNAL = "HSR-E"
|
|
17
|
-
HYBRID = "HYBRID"
|
|
15
|
+
INTERNAL = "HSR-I"
|
|
16
|
+
EXTERNAL = "HSR-E"
|
|
17
|
+
HYBRID = "HYBRID"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class SourceType(str, Enum):
|
|
21
|
-
MEMORY = "memory"
|
|
22
|
-
KNOWLEDGE = "knowledge"
|
|
23
|
-
AGENT = "agent"
|
|
24
|
-
|
|
25
|
-
WEB = "web" # 来自联网搜索(当前接的是百度 AI 搜索)
|
|
21
|
+
MEMORY = "memory"
|
|
22
|
+
KNOWLEDGE = "knowledge"
|
|
23
|
+
AGENT = "agent"
|
|
24
|
+
WEB = "web"
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@dataclass
|
|
@@ -45,11 +44,10 @@ class RawEvidence:
|
|
|
45
44
|
id: str
|
|
46
45
|
fact: str
|
|
47
46
|
source: SourceType
|
|
48
|
-
origin: str
|
|
47
|
+
origin: str
|
|
49
48
|
confidence: float = 0.5
|
|
50
49
|
raw: Any = None
|
|
51
|
-
grounded_by: Optional[str] = None
|
|
52
|
-
# "这条知识由这条 HMR 记忆溯源支撑"
|
|
50
|
+
grounded_by: Optional[str] = None
|
|
53
51
|
|
|
54
52
|
|
|
55
53
|
@dataclass
|
|
@@ -66,7 +64,7 @@ class EvidenceNode:
|
|
|
66
64
|
class EvidenceEdge:
|
|
67
65
|
src: str
|
|
68
66
|
dst: str
|
|
69
|
-
relation: str
|
|
67
|
+
relation: str
|
|
70
68
|
|
|
71
69
|
|
|
72
70
|
@dataclass
|
|
@@ -103,7 +101,7 @@ class EvidenceGraph:
|
|
|
103
101
|
|
|
104
102
|
@dataclass
|
|
105
103
|
class StrategyDecision:
|
|
106
|
-
action: str
|
|
104
|
+
action: str
|
|
107
105
|
reason: str
|
|
108
106
|
next_steps: list[SubQuery] = field(default_factory=list)
|
|
109
107
|
|
|
@@ -7,7 +7,7 @@ Query Planner —— 决定路由(HSR-I / HSR-E / HYBRID),并把任务拆成
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
from .models import SearchPlan, SubQuery, RouteType
|
|
10
|
-
from ..planning import planner as P
|
|
10
|
+
from ..planning import planner as P
|
|
11
11
|
|
|
12
12
|
_MEMORY_HINTS = ("我之前", "我们讨论过", "上次", "记得", "history", "之前说")
|
|
13
13
|
_HYBRID_HINTS = ("为什么", "对比", "分析", "综合", "评估")
|
|
@@ -28,7 +28,7 @@ class QueryPlanner:
|
|
|
28
28
|
steps = [SubQuery(id="s1", text=query)]
|
|
29
29
|
elif route == RouteType.EXTERNAL:
|
|
30
30
|
steps = self._decompose(query)
|
|
31
|
-
else:
|
|
31
|
+
else:
|
|
32
32
|
steps = [SubQuery(id="s1", text=query), *self._decompose(query, offset=2)]
|
|
33
33
|
|
|
34
34
|
return SearchPlan(
|
|
@@ -38,7 +38,7 @@ class QueryPlanner:
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
def _decompose(self, query: str, offset: int = 1) -> list[SubQuery]:
|
|
41
|
-
parts = P.split_on(query, P.LIST_SEPS)
|
|
41
|
+
parts = P.split_on(query, P.LIST_SEPS)
|
|
42
42
|
if len(parts) <= 1:
|
|
43
43
|
parts = [query]
|
|
44
44
|
return [SubQuery(id=f"s{offset + i}", text=p) for i, p in enumerate(parts)]
|
|
@@ -69,22 +69,16 @@ class SearchRuntime:
|
|
|
69
69
|
def __init__(self, hmr=None, knowledge_port=None, agent_port=None,
|
|
70
70
|
external_ports: list | None = None):
|
|
71
71
|
self.hmr = hmr
|
|
72
|
-
# knowledge_port 由 foxclaw/__init__.py 在启动时构造好传进来
|
|
73
|
-
# (进程内直连的 HKCReadPort)。这里不做任何自动连接/默认值——
|
|
74
|
-
# 连不连 HKC、怎么连,是 Foxclaw 启动流程的事,不是 SearchRuntime 的事。
|
|
75
72
|
self.knowledge_port = knowledge_port
|
|
76
|
-
# agent_port:内部搜索的「Agent」源(领域内核 + 推理链 + 各Agent记忆 + 能力/状态),
|
|
77
|
-
# 同样由 Foxclaw 启动时用各持有方引用构造好传进来。
|
|
78
73
|
self.agent_port = agent_port
|
|
79
74
|
self.planner = QueryPlanner()
|
|
80
75
|
self.fusion = EvidenceFusion()
|
|
81
76
|
self.strategy = SearchStrategy()
|
|
82
|
-
# 外部端口(联网)面向 AI/Agent:是证据/工具管线的一环,不是给人的搜索框。
|
|
83
77
|
self.external_ports = (
|
|
84
78
|
external_ports if external_ports is not None else [BaiduWebSearchPort()]
|
|
85
79
|
)
|
|
86
80
|
|
|
87
|
-
def search(self, query: str, agent_id: str = None, context: dict = None,
|
|
81
|
+
def search(self, query: str, agent_id: str | None = None, context: dict | None = None,
|
|
88
82
|
internal_only: bool = False) -> SearchResult:
|
|
89
83
|
"""唯一公开入口。给定 query,返回 SearchResult,过程中不写任何持久化状态。
|
|
90
84
|
|
|
@@ -101,7 +95,6 @@ class SearchRuntime:
|
|
|
101
95
|
trace: list[str] = []
|
|
102
96
|
plan = self.planner.plan(query, context)
|
|
103
97
|
if internal_only and plan.route == RouteType.EXTERNAL:
|
|
104
|
-
# 只查内部时,把纯外部路由拉回内部,保证内部三源(记忆/知识/Agent)被检索
|
|
105
98
|
plan.route = RouteType.INTERNAL
|
|
106
99
|
trace.append("[internal_only] 路由改写为 HSR-I(仅内部)")
|
|
107
100
|
trace.append(f"[plan] {plan.reasoning}")
|
|
@@ -11,18 +11,17 @@ Foxclaw Skill 接入层 — 助手的"万能工具箱",同时是对外生态兼
|
|
|
11
11
|
import contextvars
|
|
12
12
|
import threading
|
|
13
13
|
from dataclasses import dataclass, field, asdict
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .mcp_client import McpManager
|
|
18
|
+
from .policy import SkillPolicy
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
SkillKind = Literal["local", "mcp", "web_api"]
|
|
18
22
|
SideEffect = Literal["read", "write", "send", "pay"]
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
# ── 循环检测的会话分槽 key(阶段四·步骤13:loop_detector per-session 化)────────────────
|
|
22
|
-
# SkillAdapter 是全局单例(Foxclaw 装配一个,所有 Agent/会话共享),其循环检测历史若共享,
|
|
23
|
-
# A 会话的工具调用会污染 B 会话的"重复/横跳/无进展"判定。故用 contextvar 承载"当前会话
|
|
24
|
-
# 标识",随 asyncio Task / asyncio.to_thread 自动传播;循环检测按它分槽,消除跨会话污染。
|
|
25
|
-
# 调用方在会话/执行入口 set_loop_session(session_id),退出时 reset_loop_session(token)。
|
|
26
25
|
_loop_session: contextvars.ContextVar = contextvars.ContextVar(
|
|
27
26
|
"foxclaw_skill_loop_session", default=None)
|
|
28
27
|
|
|
@@ -43,33 +42,26 @@ class SkillSpec:
|
|
|
43
42
|
name: str
|
|
44
43
|
description: str
|
|
45
44
|
kind: SkillKind = "local"
|
|
46
|
-
triggers: list = field(default_factory=list)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
http_method: str = "GET" # web_api: GET/POST/PUT/DELETE
|
|
65
|
-
http_headers: dict = field(default_factory=dict) # web_api: 请求头(如 auth)
|
|
66
|
-
secret_ref: str = "" # web_api: 凭证引用名(存守门人 vault);空=凭证仍在 http_headers(现状)
|
|
67
|
-
auth_scheme: str = "bearer" # web_api: 凭证注入方式 bearer / header:<name> / query:<name>
|
|
68
|
-
param_location: str = "auto" # web_api: query/json/auto
|
|
69
|
-
timeout: float = 10.0 # web_api: 超时秒
|
|
70
|
-
allow_local: bool = False # web_api: 是否信任本地/内网地址(默认拒,防SSRF)
|
|
45
|
+
triggers: list = field(default_factory=list)
|
|
46
|
+
side_effect: SideEffect = "write"
|
|
47
|
+
destructive: bool = False
|
|
48
|
+
external: bool = False
|
|
49
|
+
read_only: Optional[bool] = None
|
|
50
|
+
params: dict = field(default_factory=dict)
|
|
51
|
+
handler: Optional[Callable] = None
|
|
52
|
+
compensate: Optional[Callable] = None
|
|
53
|
+
mcp_server: str = ""
|
|
54
|
+
mcp_tool: str = ""
|
|
55
|
+
endpoint: str = ""
|
|
56
|
+
http_method: str = "GET"
|
|
57
|
+
http_headers: dict = field(default_factory=dict)
|
|
58
|
+
secret_ref: str = ""
|
|
59
|
+
auth_scheme: str = "bearer"
|
|
60
|
+
param_location: str = "auto"
|
|
61
|
+
timeout: float = 10.0
|
|
62
|
+
allow_local: bool = False
|
|
71
63
|
enabled: bool = True
|
|
72
|
-
source: str = "builtin"
|
|
64
|
+
source: str = "builtin"
|
|
73
65
|
|
|
74
66
|
def trust_level(self) -> str:
|
|
75
67
|
"""信任分级(治理信号)。外部 kind 优先(不管谁注册):mcp 调外部 server=低、
|
|
@@ -94,10 +86,9 @@ class SkillResult:
|
|
|
94
86
|
skill_id: str
|
|
95
87
|
success: bool
|
|
96
88
|
output: dict = field(default_factory=dict)
|
|
97
|
-
status: str = "done"
|
|
89
|
+
status: str = "done"
|
|
98
90
|
message: str = ""
|
|
99
|
-
loop_warning: dict = None
|
|
100
|
-
# 关键操作二次确认:status=needs_confirmation 时携带一次性确认码(绑定本次动作+参数)
|
|
91
|
+
loop_warning: Optional[dict] = None
|
|
101
92
|
confirm_nonce: str = ""
|
|
102
93
|
confirm_expires_at: float = 0.0
|
|
103
94
|
|
|
@@ -110,9 +101,6 @@ class SkillRegistry:
|
|
|
110
101
|
|
|
111
102
|
def __init__(self):
|
|
112
103
|
self._skills: dict = {}
|
|
113
|
-
# 【深审修复·线程安全】Step4 引入运行时注册(sync_abilities_to_skills)后,register 会与
|
|
114
|
-
# 每次路由的 all()/match() 并发——无锁则 list 推导迭代 _skills 时若并发 register/unregister
|
|
115
|
-
# 会 "dict changed size during iteration"。RLock 保护;all() 返回快照,迭代在锁外(安全)。
|
|
116
104
|
self._lock = threading.RLock()
|
|
117
105
|
|
|
118
106
|
def register(self, spec: SkillSpec):
|
|
@@ -140,7 +128,7 @@ class SkillRegistry:
|
|
|
140
128
|
def match(self, task: str) -> Optional[SkillSpec]:
|
|
141
129
|
"""按触发词匹配最合适的 skill(命中最多者胜)。"""
|
|
142
130
|
best, best_score = None, 0
|
|
143
|
-
for s in self.all():
|
|
131
|
+
for s in self.all():
|
|
144
132
|
score = sum(1 for t in s.triggers if t in task)
|
|
145
133
|
if score > best_score:
|
|
146
134
|
best, best_score = s, score
|
|
@@ -173,31 +161,23 @@ def _mark_untrusted(out):
|
|
|
173
161
|
class SkillAdapter:
|
|
174
162
|
"""统一调用器 + 守门人。"""
|
|
175
163
|
|
|
176
|
-
def __init__(self, registry: SkillRegistry, mcp_clients: dict = None,
|
|
164
|
+
def __init__(self, registry: SkillRegistry, mcp_clients: dict | None = None,
|
|
177
165
|
rules=None, breaker=None, hooks=None, policy_gate=None, sentinel=None,
|
|
178
166
|
confirms=None):
|
|
179
167
|
self.registry = registry
|
|
180
|
-
# 关键操作(send/pay/destructive)的会话 nonce 二次确认器(core.confirm_nonce)。
|
|
181
|
-
# None → 不启用:confirmed=True 直通(既有语义,进程内调用/测试不受影响)。
|
|
182
|
-
# 外部入口(HTTP)应注入,否则「确认」只是可重放的布尔。
|
|
183
168
|
self.confirms = confirms
|
|
184
|
-
self.mcp_clients = mcp_clients or {}
|
|
185
|
-
self.policy_gate = policy_gate
|
|
186
|
-
self.sentinel = sentinel
|
|
187
|
-
self.skill_policy
|
|
188
|
-
# 权限规则集(deny>ask>allow)。宿主可注入,默认空规则。
|
|
169
|
+
self.mcp_clients: dict | McpManager = mcp_clients or {}
|
|
170
|
+
self.policy_gate = policy_gate
|
|
171
|
+
self.sentinel = sentinel
|
|
172
|
+
self.skill_policy: SkillPolicy | None = None
|
|
189
173
|
from .permission import PermissionRules
|
|
190
174
|
self.rules = rules or PermissionRules()
|
|
191
|
-
# 断路器 + 钩子(AHS FRAMEWORK 横切机制)
|
|
192
175
|
from .lifecycle import CircuitBreaker, HookManager
|
|
193
176
|
self.breaker = breaker if breaker is not None else CircuitBreaker()
|
|
194
177
|
self.hooks = hooks if hooks is not None else HookManager()
|
|
195
|
-
|
|
196
|
-
# 【阶段四·步骤13】按会话 keyed 的检测器池:全局单例 adapter 被所有 Agent/会话共享,
|
|
197
|
-
# 循环检测历史必须按 session 分槽(见 _detector_for),否则跨会话交叉污染。
|
|
198
|
-
self._loop_detectors: dict = {} # session_key -> ToolLoopDetector
|
|
178
|
+
self._loop_detectors: dict = {}
|
|
199
179
|
self._loop_lock = threading.RLock()
|
|
200
|
-
self._loop_detectors_max = 256
|
|
180
|
+
self._loop_detectors_max = 256
|
|
201
181
|
|
|
202
182
|
def _detector_for(self, session_key: Optional[str]):
|
|
203
183
|
"""取当前会话的循环检测器(按 session 分槽);无会话用共享默认槽(向后兼容)。"""
|
|
@@ -206,7 +186,6 @@ class SkillAdapter:
|
|
|
206
186
|
d = self._loop_detectors.get(key)
|
|
207
187
|
if d is None:
|
|
208
188
|
if len(self._loop_detectors) >= self._loop_detectors_max:
|
|
209
|
-
# 淘汰最久未用(插入序最前)的非默认槽,防 session 无限增长
|
|
210
189
|
for k in list(self._loop_detectors):
|
|
211
190
|
if k != "__default__":
|
|
212
191
|
del self._loop_detectors[k]
|
|
@@ -240,7 +219,7 @@ class SkillAdapter:
|
|
|
240
219
|
taint_threshold=taint_threshold)
|
|
241
220
|
|
|
242
221
|
def _audit_terminal(self, skill: SkillSpec, verdict, outcome: str,
|
|
243
|
-
reason: str, detail: dict = None) -> None:
|
|
222
|
+
reason: str, detail: dict | None = None) -> None:
|
|
244
223
|
"""【P2-0】把 skill 执行终态记入统一治理审计流(governance.decision, layer=skill)。
|
|
245
224
|
|
|
246
225
|
审计器从 policy_gate.auditor 取(standalone 无 gate → 无审计,与既有降级策略一致);
|
|
@@ -261,14 +240,12 @@ class SkillAdapter:
|
|
|
261
240
|
confirm_nonce: str = "", taint_level: int = 0,
|
|
262
241
|
taint_threshold: int = 0) -> SkillResult:
|
|
263
242
|
|
|
264
|
-
# —— 全局封杀策略(deny 压倒一切,kill-switch:所有结构化技能执行的唯一咽喉)——
|
|
265
243
|
pol = self.skill_policy
|
|
266
244
|
if pol is not None and pol.is_blocked(skill.id):
|
|
267
245
|
self._audit_terminal(skill, None, "deny", "该技能已被策略封杀(deny)",
|
|
268
246
|
{"gate": "skill_policy"})
|
|
269
247
|
return SkillResult(skill.id, False, status="blocked", message="该技能已被策略封杀(deny)")
|
|
270
248
|
|
|
271
|
-
# —— pre_skill 钩子(可阻断 / 可改 args)——
|
|
272
249
|
pre = self.hooks.fire("pre_skill", skill.id, {"args": args, "skill": skill.to_dict()})
|
|
273
250
|
if pre.blocked:
|
|
274
251
|
self._audit_terminal(skill, None, "deny", f"pre钩子阻断: {pre.reason}",
|
|
@@ -278,10 +255,6 @@ class SkillAdapter:
|
|
|
278
255
|
if pre.modified_args is not None:
|
|
279
256
|
args = pre.modified_args
|
|
280
257
|
|
|
281
|
-
# —— 关键操作会话 nonce 二次确认(必须在权限裁决**之前**)——
|
|
282
|
-
# `confirmed=True` 只说明"调用方说确认了",对 send/pay/destructive 还不够:确认码
|
|
283
|
-
# 绑定 (会话, 动作, 参数) 且单次使用 → 堵住「确认了 A 却执行 B」和「同一个 true 反复重放」。
|
|
284
|
-
# 无效/缺失 → 降级为**未确认**,走 ask 并签发新码(绝不静默放行)。
|
|
285
258
|
from ..core.confirm_nonce import is_critical as _is_critical
|
|
286
259
|
_session = _loop_session.get() or ""
|
|
287
260
|
_critical = _is_critical(getattr(skill, "side_effect", "write"),
|
|
@@ -294,10 +267,6 @@ class SkillAdapter:
|
|
|
294
267
|
confirmed = False
|
|
295
268
|
_nonce_reject = _why
|
|
296
269
|
|
|
297
|
-
# —— 四阶段权限管线(先判权限,再占断路器探针)。经统一 PolicyGate(Step4);
|
|
298
|
-
# 拿到的仍是同一 PermissionDecision(行为一致),门面额外发统一 policy.decision 事件。——
|
|
299
|
-
# 【P3-4】保留整个 Verdict:decision_id 要作为本层执行审计的 correlation_id,
|
|
300
|
-
# 把「谁批准的(permission 裁决)」与「执行结果如何(skill 终态)」join 起来。
|
|
301
270
|
verdict = None
|
|
302
271
|
if self.policy_gate is not None:
|
|
303
272
|
verdict = self.policy_gate.check_action(
|
|
@@ -326,8 +295,6 @@ class SkillAdapter:
|
|
|
326
295
|
message=_msg, **_extra)
|
|
327
296
|
_downgrade = decision.trust_downgrade
|
|
328
297
|
|
|
329
|
-
# —— 断路器:open 且未过冷却 → 走降级,不调用 ——
|
|
330
|
-
# (放在权限判定之后:deny/ask 不会执行,就不该占用 half_open 探针名额)
|
|
331
298
|
if not self.breaker.allow(skill.id):
|
|
332
299
|
st = self.breaker.status(skill.id)
|
|
333
300
|
self._audit_terminal(skill, verdict, "deny",
|
|
@@ -336,7 +303,6 @@ class SkillAdapter:
|
|
|
336
303
|
return SkillResult(skill.id, False, status="circuit_open",
|
|
337
304
|
message=f"「{skill.name}」连续失败 {st['failures']} 次,断路器已开启,暂停调用")
|
|
338
305
|
|
|
339
|
-
# —— 执行 + 记账(成功/失败驱动断路器)——
|
|
340
306
|
try:
|
|
341
307
|
res = self._dispatch(skill, args, _downgrade)
|
|
342
308
|
if res.success:
|
|
@@ -345,12 +311,10 @@ class SkillAdapter:
|
|
|
345
311
|
else:
|
|
346
312
|
self.breaker.record_failure(skill.id)
|
|
347
313
|
self.hooks.fire("on_error", skill.id, {"result": res.to_dict()})
|
|
348
|
-
# 【P2-0】执行终态审计(成功/失败都记;deny/ask 已由 permission 层记录,不重复)
|
|
349
314
|
self._audit_terminal(
|
|
350
315
|
skill, verdict, "success" if res.success else "failed",
|
|
351
316
|
res.message, {"kind": skill.kind,
|
|
352
317
|
"trust_downgrade": _downgrade})
|
|
353
|
-
# 循环检测:记录本次调用(按会话 keyed,消除跨会话污染),若检测到死循环附警告
|
|
354
318
|
loop = self._detector_for(_loop_session.get()).record(skill.id, args, res.output)
|
|
355
319
|
if loop.stuck:
|
|
356
320
|
res.loop_warning = loop.to_dict()
|
|
@@ -371,7 +335,6 @@ class SkillAdapter:
|
|
|
371
335
|
return SkillResult(skill.id, True, output=out if isinstance(out, dict)
|
|
372
336
|
else {"result": out}, message="本地 skill 执行完成")
|
|
373
337
|
elif skill.kind == "mcp":
|
|
374
|
-
# 【P-Muse·第3步】挂了守门人 → 代执行:守门人持有 MCP 凭证并调用,主进程只发意图。
|
|
375
338
|
if self.sentinel is not None:
|
|
376
339
|
res = self._invoke_mcp_via_sentinel(skill, args)
|
|
377
340
|
else:
|
|
@@ -381,8 +344,6 @@ class SkillAdapter:
|
|
|
381
344
|
res.message += "(来源不可信,结果不可驱动后续危险操作)"
|
|
382
345
|
return res
|
|
383
346
|
elif skill.kind == "web_api":
|
|
384
|
-
# 【P-Muse】声明了凭证 ref 且挂了守门人 → 代执行:真凭证只在守门人 vault,
|
|
385
|
-
# 主进程不直发请求(拿不到真值),改走 IPC 由守门人注入凭证后执行。
|
|
386
347
|
if skill.secret_ref and self.sentinel is not None:
|
|
387
348
|
return self._invoke_web_via_sentinel(skill, args)
|
|
388
349
|
return self._invoke_web(skill, args)
|
|
@@ -400,7 +361,7 @@ class SkillAdapter:
|
|
|
400
361
|
f"本地配置后即可调用 {skill.mcp_tool}")
|
|
401
362
|
try:
|
|
402
363
|
out = client.call_tool(skill.mcp_tool, args)
|
|
403
|
-
out = _mark_untrusted(out)
|
|
364
|
+
out = _mark_untrusted(out)
|
|
404
365
|
return SkillResult(skill.id, True, output=out, message="MCP 调用完成(外部不可信数据)")
|
|
405
366
|
except Exception as e:
|
|
406
367
|
return SkillResult(skill.id, False, status="blocked",
|
|
@@ -431,7 +392,6 @@ class SkillAdapter:
|
|
|
431
392
|
return SkillResult(skill.id, False, status="blocked",
|
|
432
393
|
message="缺少 httpx 依赖,无法发起 Web API 调用")
|
|
433
394
|
|
|
434
|
-
# —— SSRF 防护:只允许 http/https + 拦截内网/环回/元数据地址 ——
|
|
435
395
|
allow_local = getattr(skill, "allow_local", False)
|
|
436
396
|
block = self._ssrf_check(skill.endpoint, allow_local=allow_local)
|
|
437
397
|
if block:
|
|
@@ -439,15 +399,14 @@ class SkillAdapter:
|
|
|
439
399
|
message=f"端点被安全策略拒绝: {block}")
|
|
440
400
|
|
|
441
401
|
method = (skill.http_method or "GET").upper()
|
|
442
|
-
headers = {"User-Agent": "Foxclaw/1.0"}
|
|
443
|
-
headers.update(skill.http_headers or {})
|
|
402
|
+
headers = {"User-Agent": "Foxclaw/1.0"}
|
|
403
|
+
headers.update(skill.http_headers or {})
|
|
444
404
|
args = args or {}
|
|
445
|
-
# 参数位置:auto → GET/DELETE 走 query,POST/PUT 走 json body
|
|
446
405
|
loc = skill.param_location
|
|
447
406
|
if loc == "auto":
|
|
448
407
|
loc = "query" if method in ("GET", "DELETE") else "json"
|
|
449
|
-
req_kwargs = {"headers": headers, "timeout": skill.timeout,
|
|
450
|
-
"follow_redirects": False}
|
|
408
|
+
req_kwargs: dict[str, Any] = {"headers": headers, "timeout": skill.timeout,
|
|
409
|
+
"follow_redirects": False}
|
|
451
410
|
if loc == "query":
|
|
452
411
|
req_kwargs["params"] = args
|
|
453
412
|
else:
|
|
@@ -463,17 +422,14 @@ class SkillAdapter:
|
|
|
463
422
|
return SkillResult(skill.id, False, status="blocked",
|
|
464
423
|
message=f"Web API 请求失败: {e}")
|
|
465
424
|
|
|
466
|
-
# 3xx 重定向(已禁跟随)→ 视为需人工确认的可疑响应
|
|
467
425
|
if 300 <= resp.status_code < 400:
|
|
468
426
|
return SkillResult(skill.id, False, status="blocked",
|
|
469
427
|
message=f"Web API 返回重定向 HTTP {resp.status_code}(已阻止跟随)")
|
|
470
|
-
# 非 2xx → 失败(交给断路器计数)
|
|
471
428
|
if resp.status_code >= 400:
|
|
472
429
|
return SkillResult(skill.id, False, status="blocked",
|
|
473
430
|
message=f"Web API 返回 HTTP {resp.status_code}",
|
|
474
431
|
output={"status_code": resp.status_code,
|
|
475
432
|
"body": resp.text[:500]})
|
|
476
|
-
# 解析响应:优先 JSON,失败则文本
|
|
477
433
|
try:
|
|
478
434
|
data = resp.json()
|
|
479
435
|
except Exception:
|
|
@@ -494,12 +450,12 @@ class SkillAdapter:
|
|
|
494
450
|
message=f"守门人代执行返回异常: {d}")
|
|
495
451
|
|
|
496
452
|
@staticmethod
|
|
497
|
-
def _ssrf_check(url: str, allow_local: bool = False) -> str:
|
|
453
|
+
def _ssrf_check(url: str, allow_local: bool = False) -> str | None:
|
|
498
454
|
"""返回拦截原因(非空=拒绝),None=放行。委托模块级 ssrf_check(单一真源)。"""
|
|
499
455
|
return ssrf_check(url, allow_local=allow_local)
|
|
500
456
|
|
|
501
457
|
|
|
502
|
-
def ssrf_check(url: str, allow_local: bool = False) -> str:
|
|
458
|
+
def ssrf_check(url: str, allow_local: bool = False) -> str | None:
|
|
503
459
|
"""SSRF 防护(单一真源):返回拦截原因(非空=拒绝),None=放行。拦截非 http(s)/内网/环回/元数据。
|
|
504
460
|
|
|
505
461
|
主进程 SkillAdapter 与守门人进程共用 —— 守门人代执行时,主进程可能被攻破传内网地址,
|
|
@@ -517,39 +473,31 @@ def ssrf_check(url: str, allow_local: bool = False) -> str:
|
|
|
517
473
|
if not host:
|
|
518
474
|
return "缺少 host"
|
|
519
475
|
if allow_local:
|
|
520
|
-
return None
|
|
521
|
-
# 明确的危险主机名
|
|
476
|
+
return None
|
|
522
477
|
if host.lower() in ("localhost", "metadata.google.internal"):
|
|
523
478
|
return f"内部主机: {host}"
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
"fd00:ec2::254"} # IPv6 云元数据
|
|
479
|
+
CLOUD_METADATA = {"169.254.169.254", "100.100.100.200",
|
|
480
|
+
"fd00:ec2::254"}
|
|
527
481
|
if host in CLOUD_METADATA:
|
|
528
482
|
return f"云元数据地址: {host}"
|
|
529
|
-
# IP 地址 → 拦截私网/环回/链路本地/元数据
|
|
530
483
|
try:
|
|
531
484
|
ip = ipaddress.ip_address(host)
|
|
532
485
|
if (ip.is_private or ip.is_loopback or ip.is_link_local
|
|
533
486
|
or ip.is_reserved or ip.is_multicast):
|
|
534
487
|
return f"内网/保留地址: {host}"
|
|
535
|
-
# carrier-grade NAT 100.64.0.0/10(标准 is_private 不含,阿里云元数据在此段)
|
|
536
488
|
if isinstance(ip, ipaddress.IPv4Address) and \
|
|
537
489
|
ip in ipaddress.ip_network("100.64.0.0/10"):
|
|
538
490
|
return f"运营商级 NAT 段: {host}"
|
|
539
491
|
except ValueError:
|
|
540
|
-
pass
|
|
492
|
+
pass
|
|
541
493
|
return None
|
|
542
494
|
|
|
543
495
|
|
|
544
|
-
# ── 内置示例 skill(演示用,真实 MCP/API 留接口)─────────────
|
|
545
496
|
def _demo_calc(expression: str = "") -> dict:
|
|
546
497
|
"""安全的演示计算 skill。"""
|
|
547
498
|
allowed = set("0123456789+-*/(). ")
|
|
548
499
|
if not set(expression) <= allowed:
|
|
549
500
|
return {"error": "仅支持数字和基本运算符"}
|
|
550
|
-
# 字符白名单只挡住"取名字/取属性"(字母被禁),挡不住**计算量攻击**:
|
|
551
|
-
# `9**9**9` 这类表达式在 eval 阶段就会长时间计算并吃满内存 —— 属 R2 检测出的低危项。
|
|
552
|
-
# 故在 eval 之前限长度、限连续乘方、限指数大小;eval 之后再兜底检查结果规模。
|
|
553
501
|
if len(expression) > 200:
|
|
554
502
|
return {"error": "表达式过长"}
|
|
555
503
|
if expression.count("**") > 1:
|
|
@@ -574,14 +522,12 @@ def build_default_registry() -> SkillRegistry:
|
|
|
574
522
|
"""构建默认 skill 注册表:演示本地 skill + 留口的 MCP skill。"""
|
|
575
523
|
reg = SkillRegistry()
|
|
576
524
|
|
|
577
|
-
# 本地只读 skill(直接可用)
|
|
578
525
|
reg.register(SkillSpec(
|
|
579
526
|
id="calculator", name="计算器", description="数学表达式计算",
|
|
580
527
|
kind="local", triggers=["计算", "算一下", "算", "等于多少", "加", "减", "乘", "除"],
|
|
581
528
|
side_effect="read", handler=_demo_calc,
|
|
582
529
|
params={"expression": "str"}))
|
|
583
530
|
|
|
584
|
-
# MCP skill(留接口,本地配 server 后可用)——对齐我们接过的生态
|
|
585
531
|
reg.register(SkillSpec(
|
|
586
532
|
id="amap_route", name="高德路线", description="查询两地真实路线和时间",
|
|
587
533
|
kind="mcp", triggers=["路线", "怎么走", "多久到", "导航"],
|