foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -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/RELEASE.md +4 -3
- 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 +34 -152
- package/python/foxclaw/agents/base.py +65 -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 +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- 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 +28 -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/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/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 +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- 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/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 +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +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 +0 -1
- 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 +0 -41
- 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 +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- 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_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_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_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/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/release_check.py +7 -7
- 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/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -18,20 +18,20 @@ from collections import deque
|
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
from typing import Callable, Optional, Literal
|
|
20
20
|
|
|
21
|
+
from .hooks_spec import HookEvent
|
|
21
22
|
|
|
22
|
-
# ============ 断路器 ============
|
|
23
23
|
|
|
24
24
|
CircuitState = Literal["closed", "open", "half_open"]
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
|
28
28
|
class _BreakerEntry:
|
|
29
|
-
failures: int = 0
|
|
29
|
+
failures: int = 0
|
|
30
30
|
state: CircuitState = "closed"
|
|
31
|
-
opened_at: float = 0.0
|
|
32
|
-
probe_in_flight: bool = False
|
|
33
|
-
probe_started_at: float = 0.0
|
|
34
|
-
probe_successes: int = 0
|
|
31
|
+
opened_at: float = 0.0
|
|
32
|
+
probe_in_flight: bool = False
|
|
33
|
+
probe_started_at: float = 0.0
|
|
34
|
+
probe_successes: int = 0
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class CircuitBreaker:
|
|
@@ -48,10 +48,10 @@ class CircuitBreaker:
|
|
|
48
48
|
recovery_threshold: int = 1, probe_timeout: float = 60.0):
|
|
49
49
|
self.threshold = threshold
|
|
50
50
|
self.cooldown = cooldown
|
|
51
|
-
self.recovery_threshold = max(1, recovery_threshold)
|
|
52
|
-
self.probe_timeout = probe_timeout
|
|
51
|
+
self.recovery_threshold = max(1, recovery_threshold)
|
|
52
|
+
self.probe_timeout = probe_timeout
|
|
53
53
|
self._entries: dict[str, _BreakerEntry] = {}
|
|
54
|
-
self._lock = threading.RLock()
|
|
54
|
+
self._lock = threading.RLock()
|
|
55
55
|
|
|
56
56
|
def _get(self, sid: str) -> _BreakerEntry:
|
|
57
57
|
if sid not in self._entries:
|
|
@@ -71,17 +71,14 @@ class CircuitBreaker:
|
|
|
71
71
|
return True
|
|
72
72
|
if e.state == "open":
|
|
73
73
|
if time.time() - e.opened_at >= self.cooldown:
|
|
74
|
-
# 冷却满 → 真正进入 half_open,放行第一个探针
|
|
75
74
|
e.state = "half_open"
|
|
76
75
|
e.probe_in_flight = True
|
|
77
76
|
e.probe_started_at = time.time()
|
|
78
77
|
e.probe_successes = 0
|
|
79
78
|
return True
|
|
80
79
|
return False
|
|
81
|
-
# half_open:已有探针在飞 → 拒绝其余;否则放行一个新探针
|
|
82
80
|
if e.state == "half_open":
|
|
83
81
|
if e.probe_in_flight:
|
|
84
|
-
# 探针超时未记账(调用方崩溃/漏记)→ 视为丢失,释放以免永久卡死
|
|
85
82
|
if time.time() - e.probe_started_at >= self.probe_timeout:
|
|
86
83
|
e.probe_in_flight = False
|
|
87
84
|
else:
|
|
@@ -98,11 +95,9 @@ class CircuitBreaker:
|
|
|
98
95
|
e.probe_in_flight = False
|
|
99
96
|
e.probe_successes += 1
|
|
100
97
|
if e.probe_successes >= self.recovery_threshold:
|
|
101
|
-
# 探针连续成功达标 → 恢复
|
|
102
98
|
e.state = "closed"
|
|
103
99
|
e.failures = 0
|
|
104
100
|
e.probe_successes = 0
|
|
105
|
-
# 否则保持 half_open,等下一个探针
|
|
106
101
|
else:
|
|
107
102
|
e.failures = 0
|
|
108
103
|
e.state = "closed"
|
|
@@ -111,7 +106,6 @@ class CircuitBreaker:
|
|
|
111
106
|
with self._lock:
|
|
112
107
|
e = self._get(sid)
|
|
113
108
|
if e.state == "half_open":
|
|
114
|
-
# 探针失败 → 回到 open,重新计冷却
|
|
115
109
|
e.state = "open"
|
|
116
110
|
e.opened_at = time.time()
|
|
117
111
|
e.probe_in_flight = False
|
|
@@ -126,7 +120,6 @@ class CircuitBreaker:
|
|
|
126
120
|
with self._lock:
|
|
127
121
|
e = self._get(sid)
|
|
128
122
|
state = e.state
|
|
129
|
-
# open 但冷却已满 → 对外显示 half_open(下次 allow 会真正转入)
|
|
130
123
|
if e.state == "open" and time.time() - e.opened_at >= self.cooldown:
|
|
131
124
|
state = "half_open"
|
|
132
125
|
return {"state": state, "failures": e.failures,
|
|
@@ -154,20 +147,11 @@ class CircuitBreaker:
|
|
|
154
147
|
return False
|
|
155
148
|
|
|
156
149
|
|
|
157
|
-
# ============ 生命周期钩子 ============
|
|
158
|
-
|
|
159
|
-
HookEvent = Literal["before_tool_call", "after_tool_call", "on_error", "on_loop",
|
|
160
|
-
"session_start", "agent_start", "agent_end", "session_end",
|
|
161
|
-
"before_message_write",
|
|
162
|
-
# 向后兼容的旧名 + E2 语义别名(内部规整为标准名)
|
|
163
|
-
"pre_skill", "post_skill", "pre_reasoning", "task_complete"]
|
|
164
|
-
|
|
165
|
-
|
|
166
150
|
@dataclass
|
|
167
151
|
class Hook:
|
|
168
152
|
event: HookEvent
|
|
169
|
-
matcher: str
|
|
170
|
-
handler: Callable
|
|
153
|
+
matcher: str
|
|
154
|
+
handler: Callable
|
|
171
155
|
name: str = ""
|
|
172
156
|
|
|
173
157
|
def matches(self, skill_id: str) -> bool:
|
|
@@ -176,10 +160,10 @@ class Hook:
|
|
|
176
160
|
|
|
177
161
|
@dataclass
|
|
178
162
|
class HookResult:
|
|
179
|
-
blocked: bool = False
|
|
163
|
+
blocked: bool = False
|
|
180
164
|
reason: str = ""
|
|
181
165
|
modified_args: Optional[dict] = None
|
|
182
|
-
additional_context: Optional[str] = None
|
|
166
|
+
additional_context: Optional[str] = None
|
|
183
167
|
|
|
184
168
|
|
|
185
169
|
class HookManager:
|
|
@@ -195,18 +179,13 @@ class HookManager:
|
|
|
195
179
|
def __init__(self, timeout: float = 10.0, audit_limit: int = 2000):
|
|
196
180
|
self.timeout = timeout
|
|
197
181
|
self._hooks: list[Hook] = []
|
|
198
|
-
# 【深审修复·长期运行泄漏】audit_log 原为普通 list、只增不减:每次 skill invoke 触发
|
|
199
|
-
# pre/post/error/loop 等钩子都 append,长跑(尤其 V2 管线内 dispatch 高频用工具)无界
|
|
200
|
-
# 增长 → 内存泄漏(与 TruthCrystallizer._pending 同类)。改有界 deque:保留最近 N 条,
|
|
201
|
-
# 溢出自动丢弃最老的;deque.append 原子 ⇒ 并发 fire 也线程安全,无需额外锁。
|
|
202
182
|
self.audit_log: deque = deque(maxlen=max(100, audit_limit))
|
|
203
183
|
|
|
204
184
|
def register(self, event: HookEvent, handler: Callable,
|
|
205
185
|
matcher: str = "*", name: str = ""):
|
|
206
186
|
from .hooks_spec import normalize_hook, is_valid_hook
|
|
207
|
-
norm = normalize_hook(event)
|
|
187
|
+
norm = normalize_hook(event)
|
|
208
188
|
if not is_valid_hook(norm):
|
|
209
|
-
# 非法钩子名(可能拼写错误)→ 记录警告,避免静默失效难以排查
|
|
210
189
|
self.audit_log.append({
|
|
211
190
|
"event": event, "hook": name or getattr(handler, "__name__", "?"),
|
|
212
191
|
"matcher": matcher, "duration_ms": 0, "timed_out": False,
|
|
@@ -225,7 +204,6 @@ class HookManager:
|
|
|
225
204
|
except Exception as e:
|
|
226
205
|
err = str(e)
|
|
227
206
|
dt = time.time() - t0
|
|
228
|
-
# 超时只记录(同步 hook 无法真正中断,但记录超时供审计)
|
|
229
207
|
timed_out = dt > self.timeout
|
|
230
208
|
self.audit_log.append({
|
|
231
209
|
"event": hook.event, "hook": hook.name, "matcher": hook.matcher,
|
|
@@ -234,7 +212,7 @@ class HookManager:
|
|
|
234
212
|
})
|
|
235
213
|
return {} if err else outcome
|
|
236
214
|
|
|
237
|
-
def fire(self, event: HookEvent, skill_id: str = "", ctx: dict = None) -> HookResult:
|
|
215
|
+
def fire(self, event: HookEvent, skill_id: str = "", ctx: dict | None = None) -> HookResult:
|
|
238
216
|
"""触发某事件的所有匹配 hook。仅 before_tool_call(原 pre_skill)的 block 生效;
|
|
239
217
|
CONTEXT_HOOKS(pre_reasoning/agent_start)返回的 additional_context 会被收集拼接。
|
|
240
218
|
|
|
@@ -245,18 +223,16 @@ class HookManager:
|
|
|
245
223
|
"""
|
|
246
224
|
from .hooks_spec import (normalize_hook, BLOCKING_HOOKS, TOOL_SCOPED_HOOKS,
|
|
247
225
|
CONTEXT_HOOKS)
|
|
248
|
-
event = normalize_hook(event)
|
|
226
|
+
event = normalize_hook(event)
|
|
249
227
|
ctx = dict(ctx or {}, event=event, skill_id=skill_id)
|
|
250
228
|
result = HookResult()
|
|
251
229
|
ctx_parts = []
|
|
252
230
|
for hook in self._hooks:
|
|
253
231
|
if hook.event != event:
|
|
254
232
|
continue
|
|
255
|
-
# 工具级钩子按 skill_id 过滤(matcher)
|
|
256
233
|
if event in TOOL_SCOPED_HOOKS and not hook.matches(skill_id):
|
|
257
234
|
continue
|
|
258
235
|
out = self._run_one(hook, ctx)
|
|
259
|
-
# 仅有阻断权的钩子(before_tool_call)的 block / modified_args 生效
|
|
260
236
|
if event in BLOCKING_HOOKS:
|
|
261
237
|
if out.get("block"):
|
|
262
238
|
result.blocked = True
|
|
@@ -264,7 +240,6 @@ class HookManager:
|
|
|
264
240
|
return result
|
|
265
241
|
if out.get("modified_args") is not None:
|
|
266
242
|
result.modified_args = out["modified_args"]
|
|
267
|
-
# 可注入上下文的钩子:收集 additional_context(兼容驼峰 additionalContext)
|
|
268
243
|
if event in CONTEXT_HOOKS:
|
|
269
244
|
inj = out.get("additional_context") or out.get("additionalContext")
|
|
270
245
|
if inj:
|
|
@@ -274,5 +249,4 @@ class HookManager:
|
|
|
274
249
|
return result
|
|
275
250
|
|
|
276
251
|
def recent_audit(self, n: int = 20) -> list:
|
|
277
|
-
# deque 不支持负索引切片,先快照成 list(原子快照,并发安全)再取尾 N 条
|
|
278
252
|
return list(self.audit_log)[-n:]
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
import threading
|
|
5
5
|
from dataclasses import dataclass, field, asdict
|
|
6
6
|
import yaml
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@dataclass
|
|
@@ -15,11 +16,11 @@ class MarkdownSkill:
|
|
|
15
16
|
body: str = ""
|
|
16
17
|
enabled: bool = True
|
|
17
18
|
source: str = "user"
|
|
18
|
-
version: str = ""
|
|
19
|
-
builtin: bool = False
|
|
19
|
+
version: str = ""
|
|
20
|
+
builtin: bool = False
|
|
20
21
|
|
|
21
22
|
def to_dict(self) -> dict:
|
|
22
|
-
d = asdict(self); d["trust"] = "low"; return d
|
|
23
|
+
d = asdict(self); d["trust"] = "low"; return d
|
|
23
24
|
|
|
24
25
|
def to_meta(self) -> dict:
|
|
25
26
|
d = self.to_dict(); d.pop("body", None); return d
|
|
@@ -29,7 +30,7 @@ def _slug(s: str) -> str:
|
|
|
29
30
|
return re.sub(r"[^a-z0-9]+", "_", (s or "").lower()).strip("_") or "skill"
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def parse_skill_md(text: str, id: str = None) -> MarkdownSkill:
|
|
33
|
+
def parse_skill_md(text: str, id: str | None = None) -> MarkdownSkill:
|
|
33
34
|
"""解析 SKILL.md(--- yaml frontmatter --- + 正文)。缺 name/description → ValueError。"""
|
|
34
35
|
text = (text or "").lstrip("").strip()
|
|
35
36
|
m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.DOTALL)
|
|
@@ -65,7 +66,7 @@ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
|
|
|
65
66
|
用户已安装的同 id 技能优先(不覆盖),内置只是缺省值。
|
|
66
67
|
"""
|
|
67
68
|
from pathlib import Path
|
|
68
|
-
loaded = []
|
|
69
|
+
loaded: list[Any] = []
|
|
69
70
|
try:
|
|
70
71
|
base = Path(root)
|
|
71
72
|
if not base.is_dir():
|
|
@@ -74,7 +75,7 @@ def load_builtin_dir(reg: MarkdownSkillRegistry, root) -> list:
|
|
|
74
75
|
try:
|
|
75
76
|
s = parse_skill_md(p.read_text(encoding="utf-8", errors="replace"))
|
|
76
77
|
except Exception:
|
|
77
|
-
continue
|
|
78
|
+
continue
|
|
78
79
|
if not s.builtin or reg.get(s.id) is not None:
|
|
79
80
|
continue
|
|
80
81
|
reg.register(s)
|
|
@@ -88,7 +89,7 @@ class MarkdownSkillRegistry:
|
|
|
88
89
|
def __init__(self):
|
|
89
90
|
self._skills = {}
|
|
90
91
|
self._lock = threading.RLock()
|
|
91
|
-
self.policy = None
|
|
92
|
+
self.policy = None
|
|
92
93
|
|
|
93
94
|
def register(self, s: MarkdownSkill):
|
|
94
95
|
with self._lock:
|
|
@@ -119,7 +120,7 @@ class MarkdownSkillRegistry:
|
|
|
119
120
|
if not s.enabled:
|
|
120
121
|
continue
|
|
121
122
|
if pol is not None and pol.is_blocked(s.id):
|
|
122
|
-
continue
|
|
123
|
+
continue
|
|
123
124
|
if s.triggers:
|
|
124
125
|
if any(k.lower() in t_low for k in s.triggers):
|
|
125
126
|
out.append(s)
|
|
@@ -164,7 +165,7 @@ def extract_skill_package(zip_bytes: bytes, max_uncompressed: int = 2_000_000):
|
|
|
164
165
|
if not skill_name:
|
|
165
166
|
raise ValueError("包内未找到 SKILL.md")
|
|
166
167
|
skill_md = zf.read(skill_name).decode("utf-8", "replace")
|
|
167
|
-
meta = {}
|
|
168
|
+
meta: dict[str, Any] = {}
|
|
168
169
|
meta_name = next((n for n in zf.namelist() if n.lower().endswith("_meta.json")), None)
|
|
169
170
|
if meta_name:
|
|
170
171
|
try:
|
|
@@ -11,7 +11,6 @@ from mcp.client.streamable_http import streamablehttp_client
|
|
|
11
11
|
from mcp.client.sse import sse_client
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
# ── async→sync 桥:单例后台 loop 线程 ──────────────────────────
|
|
15
14
|
class _Bridge:
|
|
16
15
|
def __init__(self):
|
|
17
16
|
self._loop = asyncio.new_event_loop()
|
|
@@ -27,7 +26,7 @@ class _Bridge:
|
|
|
27
26
|
try:
|
|
28
27
|
return fut.result(timeout=timeout)
|
|
29
28
|
except BaseException:
|
|
30
|
-
fut.cancel()
|
|
29
|
+
fut.cancel()
|
|
31
30
|
raise
|
|
32
31
|
|
|
33
32
|
|
|
@@ -43,7 +42,6 @@ def _get_bridge() -> _Bridge:
|
|
|
43
42
|
return _bridge
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
# ── SSRF 防线:连接(含重定向的每一跳)都校验目标 host 不是内网/本地/元数据 ──────
|
|
47
45
|
async def _ssrf_request_hook(request):
|
|
48
46
|
"""httpx 请求钩子:对每个即将发出的请求(初始 + 重定向 follow-up)校验 host。
|
|
49
47
|
堵 redirect-SSRF + DNS 重绑定 + 配置漂移(恶意 server 把你导去内网/云元数据)。"""
|
|
@@ -59,12 +57,11 @@ def _safe_httpx_factory(headers=None, timeout=None, auth=None):
|
|
|
59
57
|
headers=headers,
|
|
60
58
|
timeout=timeout if timeout is not None else httpx.Timeout(30.0),
|
|
61
59
|
auth=auth,
|
|
62
|
-
follow_redirects=True,
|
|
60
|
+
follow_redirects=True,
|
|
63
61
|
event_hooks={"request": [_ssrf_request_hook]},
|
|
64
62
|
)
|
|
65
63
|
|
|
66
64
|
|
|
67
|
-
# ── 底层 async 助手(测试可 monkeypatch 这两个) ─────────────────
|
|
68
65
|
@asynccontextmanager
|
|
69
66
|
async def _connect(url, headers, transport):
|
|
70
67
|
if transport == "sse":
|
|
@@ -98,14 +95,13 @@ def _norm(cfg: dict) -> dict:
|
|
|
98
95
|
"transport": cfg.get("transport") or "streamable_http"}
|
|
99
96
|
|
|
100
97
|
|
|
101
|
-
# ── 工具 → SkillSpec(fail-closed) ─────────────────────────────
|
|
102
98
|
def _get(tool, key, default=None):
|
|
103
99
|
"""兼容 dict(守门人序列化后) 与 mcp.types.Tool 对象(直连回退路径)。"""
|
|
104
100
|
return tool.get(key, default) if isinstance(tool, dict) else getattr(tool, key, default)
|
|
105
101
|
|
|
106
102
|
|
|
107
103
|
def tool_to_spec(server_id: str, tool):
|
|
108
|
-
from . import SkillSpec
|
|
104
|
+
from . import SideEffect, SkillSpec
|
|
109
105
|
ann = _get(tool, "annotations", None)
|
|
110
106
|
if isinstance(ann, dict):
|
|
111
107
|
ro = ann.get("readOnlyHint")
|
|
@@ -113,12 +109,13 @@ def tool_to_spec(server_id: str, tool):
|
|
|
113
109
|
else:
|
|
114
110
|
ro = getattr(ann, "readOnlyHint", None) if ann else None
|
|
115
111
|
de = getattr(ann, "destructiveHint", None) if ann else None
|
|
112
|
+
se: SideEffect
|
|
116
113
|
if ro is True:
|
|
117
114
|
se, destr = "read", False
|
|
118
115
|
elif de is True:
|
|
119
116
|
se, destr = "write", True
|
|
120
117
|
else:
|
|
121
|
-
se, destr = "write", False
|
|
118
|
+
se, destr = "write", False
|
|
122
119
|
tname = _get(tool, "name", "") or ""
|
|
123
120
|
return SkillSpec(
|
|
124
121
|
id=f"mcp__{server_id}__{tname}", name=tname,
|
|
@@ -129,7 +126,6 @@ def tool_to_spec(server_id: str, tool):
|
|
|
129
126
|
)
|
|
130
127
|
|
|
131
128
|
|
|
132
|
-
# ── 供 SkillAdapter 的客户端代理 ──────────────────────────────
|
|
133
129
|
class _ClientProxy:
|
|
134
130
|
def __init__(self, mgr, sid):
|
|
135
131
|
self._mgr, self._sid = mgr, sid
|
|
@@ -140,8 +136,6 @@ class _ClientProxy:
|
|
|
140
136
|
raise RuntimeError(f"MCP server {self._sid} 未登记")
|
|
141
137
|
from foxclaw.core.tracing import span as _span, current_traceparent
|
|
142
138
|
with _span("mcp.call_tool", server=self._sid, tool=str(tool)):
|
|
143
|
-
# 跨进程传播:带上 W3C traceparent,对端(外部 MCP server)若支持即可接上同一条链。
|
|
144
|
-
# 这是 Foxclaw 唯一真实的跨进程边界,也是"分布式追踪"在本系统里唯一有意义之处。
|
|
145
139
|
headers = dict(cfg["headers"] or {})
|
|
146
140
|
tp = current_traceparent()
|
|
147
141
|
if tp:
|
|
@@ -28,9 +28,6 @@ class McpStore:
|
|
|
28
28
|
def save(self, servers: list):
|
|
29
29
|
with self._lock:
|
|
30
30
|
try:
|
|
31
|
-
# 【P-Muse·第4步】守门人模式下主进程只存元数据(headers 为空),正常不触发。
|
|
32
|
-
# 一旦有 headers 非空,说明走的是回退路径(或误传凭证)——明文凭证即将落盘,
|
|
33
|
-
# 主动告警,消除"凭证静默落盘、重启后才暴露"的隐患。
|
|
34
31
|
if any(dict(s.get("headers") or {}) for s in servers):
|
|
35
32
|
print("[McpStore] ⚠ 检测到 headers 凭证,将明文落盘到 "
|
|
36
33
|
f"{self._path.name}(当前为回退路径/未启用守门人)。",
|
|
@@ -16,25 +16,20 @@ from typing import Literal, Optional
|
|
|
16
16
|
|
|
17
17
|
Decision = Literal["allow", "ask", "deny"]
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ASK_RULE = "ASK_RULE" # 命中宿主 ask 规则
|
|
26
|
-
EXTERNAL_CONFIRM = "EXTERNAL_CONFIRM" # 外部调用,需确认
|
|
27
|
-
TAINT_CONFIRM = "TAINT_CONFIRM" # 会话已取回敏感数据,外发/副作用须二次确认
|
|
28
|
-
# deny / allow 侧文稿未定义 code,保守留 "":deny 的铁律语义(deny 不可覆盖)已由
|
|
29
|
-
# decision + stage 充分表达,不擅自扩大枚举面。
|
|
19
|
+
MISSING_PARAMS = "MISSING_PARAMS"
|
|
20
|
+
DESTRUCTIVE_CONFIRM = "DESTRUCTIVE_CONFIRM"
|
|
21
|
+
SIDE_EFFECT_CONFIRM = "SIDE_EFFECT_CONFIRM"
|
|
22
|
+
ASK_RULE = "ASK_RULE"
|
|
23
|
+
EXTERNAL_CONFIRM = "EXTERNAL_CONFIRM"
|
|
24
|
+
TAINT_CONFIRM = "TAINT_CONFIRM"
|
|
30
25
|
|
|
31
26
|
|
|
32
27
|
@dataclass
|
|
33
28
|
class PermissionRules:
|
|
34
29
|
"""宿主提供的规则集。deny 优先级最高且不可覆盖。"""
|
|
35
|
-
deny: list = field(default_factory=list)
|
|
36
|
-
ask: list = field(default_factory=list)
|
|
37
|
-
allow: list = field(default_factory=list)
|
|
30
|
+
deny: list = field(default_factory=list)
|
|
31
|
+
ask: list = field(default_factory=list)
|
|
32
|
+
allow: list = field(default_factory=list)
|
|
38
33
|
|
|
39
34
|
@staticmethod
|
|
40
35
|
def _match(patterns: list, skill_id: str) -> bool:
|
|
@@ -54,11 +49,10 @@ class PermissionRules:
|
|
|
54
49
|
@dataclass
|
|
55
50
|
class PermissionDecision:
|
|
56
51
|
decision: Decision
|
|
57
|
-
stage: int
|
|
52
|
+
stage: int
|
|
58
53
|
reason: str = ""
|
|
59
|
-
trust_downgrade: bool = False
|
|
54
|
+
trust_downgrade: bool = False
|
|
60
55
|
requires_confirmation: bool = False
|
|
61
|
-
# 【P5-1】ask 的结构化原因码(见模块顶部常量);非 ask 或文稿未定义的情形为 ""
|
|
62
56
|
reason_code: str = ""
|
|
63
57
|
|
|
64
58
|
def to_dict(self) -> dict:
|
|
@@ -85,9 +79,6 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
|
|
|
85
79
|
rules = rules or PermissionRules()
|
|
86
80
|
sid = skill.id
|
|
87
81
|
|
|
88
|
-
# ---------- Stage 1: validate(输入合法性)----------
|
|
89
|
-
# 参数 schema 基本校验:声明的必填参数是否齐全
|
|
90
|
-
# 兼容两种 params 格式:{"k": "str"}(简单)或 {"k": {"required": True}}(详细)
|
|
91
82
|
params = getattr(skill, "params", {}) or {}
|
|
92
83
|
missing = []
|
|
93
84
|
for k, spec in params.items():
|
|
@@ -95,59 +86,37 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
|
|
|
95
86
|
if required and k not in (args or {}):
|
|
96
87
|
missing.append(k)
|
|
97
88
|
if missing:
|
|
98
|
-
# 校验失败 → 优雅降级为 ask(不是安全拒绝,是数据不全)
|
|
99
|
-
# 【P5-1】MISSING_PARAMS:确认无法绕过缺参数,调用方须先补数据
|
|
100
89
|
return PermissionDecision("ask", 1,
|
|
101
90
|
reason=f"缺少必填参数: {', '.join(missing)}",
|
|
102
91
|
requires_confirmation=True, reason_code=MISSING_PARAMS)
|
|
103
92
|
|
|
104
|
-
# ---------- Stage 2: rule-match(deny > ask > allow 铁律)----------
|
|
105
93
|
if rules.hits_deny(sid):
|
|
106
94
|
return PermissionDecision("deny", 2, reason=f"命中 deny 规则(不可覆盖): {sid}")
|
|
107
95
|
ask_by_rule = rules.hits_ask(sid)
|
|
108
96
|
allow_by_rule = rules.hits_allow(sid)
|
|
109
|
-
# deny 不命中时,ask 优先于 allow
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
side_effect = getattr(skill, "side_effect", "write") # 未声明 → 按有副作用(fail-closed)
|
|
98
|
+
side_effect = getattr(skill, "side_effect", "write")
|
|
113
99
|
destructive = getattr(skill, "destructive", False)
|
|
114
100
|
external = getattr(skill, "external", False)
|
|
115
|
-
# read_only 字段默认 None → 按 side_effect 推断(read 即只读);显式 True/False 则尊重
|
|
116
101
|
_ro = getattr(skill, "read_only", None)
|
|
117
102
|
read_only = (side_effect == "read") if _ro is None else _ro
|
|
118
|
-
# 单一真源(P6 修复):信任分级由 SkillSpec.trust_level() 唯一裁定。此前 permission 另存一份
|
|
119
|
-
# TRUST_LEVEL dict,与 trust_level() 重复且含死 key(bundled/remote —— kind 永不取此二值);对 3 个
|
|
120
|
-
# 合法 SkillKind(local/web_api/mcp)二者 downgrade 行为完全一致,故收敛为单一真源、行为不变。
|
|
121
|
-
# duck-typed skill 无 trust_level 方法时 fail-closed 当低信任。
|
|
122
|
-
# 【P5-0】trust_level() 抛异常 → 当低信任(fail-closed),不让权限管线崩。
|
|
123
|
-
# 第三方 skill 的 trust_level() 若有 bug(如 AttributeError),原实现会让异常直接冒泡、
|
|
124
|
-
# evaluate 崩溃,用户看到「调用失败」而非「权限拒绝」——这是 fail-open。改为兜住异常、
|
|
125
|
-
# 按最低信任(low)处理:无法判定信任等级时,宁可保守。
|
|
126
103
|
_tl = getattr(skill, "trust_level", None)
|
|
127
104
|
try:
|
|
128
105
|
trust = _tl() if callable(_tl) else "low"
|
|
129
106
|
except Exception:
|
|
130
107
|
trust = "low"
|
|
131
|
-
downgrade = trust == "low"
|
|
108
|
+
downgrade = trust == "low"
|
|
132
109
|
|
|
133
|
-
# readonly 模式:非只读一律拒绝
|
|
134
110
|
if mode == "readonly" and not read_only:
|
|
135
111
|
return PermissionDecision("deny", 3,
|
|
136
112
|
reason="readonly 模式下禁止非只读操作", trust_downgrade=downgrade)
|
|
137
113
|
|
|
138
|
-
# destructive 永不自动放行(即使有 allow 规则也要确认)
|
|
139
114
|
if destructive and not confirmed:
|
|
140
115
|
return PermissionDecision("ask", 3,
|
|
141
116
|
reason="不可逆(destructive)操作,必须用户确认",
|
|
142
117
|
trust_downgrade=downgrade, requires_confirmation=True,
|
|
143
118
|
reason_code=DESTRUCTIVE_CONFIRM)
|
|
144
119
|
|
|
145
|
-
# Stage 3.5 污点门槛(Muse 式「碰过敏感数据 → 外发动作追加确认」):
|
|
146
|
-
# 会话已取回 ≥threshold 的用户数据,再外发/副作用 → 即使工具本身可信也强制确认,
|
|
147
|
-
# 防「模型把用户隐私塞进 send/write/external」外泄。置于 trusted 之前:
|
|
148
|
-
# 污点是结构性约束,trusted 只信任工具本身,不豁免「携带敏感数据外发」。
|
|
149
|
-
# [P-Muse·确认放行] `not confirmed`:此闸防的是**静默**外发,用户显式确认后必须放行,
|
|
150
|
-
# 否则「问一次 → 确认 → 再问一次」永远出不去(与上方 destructive 的判定口径一致)。
|
|
151
120
|
if taint_threshold and taint_level >= taint_threshold and not confirmed:
|
|
152
121
|
if side_effect in ("write", "send", "pay") or external:
|
|
153
122
|
return PermissionDecision("ask", 3,
|
|
@@ -155,26 +124,22 @@ def evaluate(skill, args: dict, rules: Optional[PermissionRules] = None,
|
|
|
155
124
|
trust_downgrade=downgrade, requires_confirmation=True,
|
|
156
125
|
reason_code=TAINT_CONFIRM)
|
|
157
126
|
|
|
158
|
-
# trusted 模式:跳过 ask,只保留 deny(上面已查)+ destructive(上面已查)
|
|
159
127
|
if mode == "trusted":
|
|
160
128
|
return PermissionDecision("allow", 3,
|
|
161
129
|
reason="trusted 模式放行", trust_downgrade=downgrade)
|
|
162
130
|
|
|
163
|
-
# ---------- Stage 4: confirm(确认)----------
|
|
164
131
|
needs_confirm = (
|
|
165
132
|
ask_by_rule or external or side_effect in ("write", "send", "pay")
|
|
166
133
|
)
|
|
167
134
|
if needs_confirm and not confirmed:
|
|
168
135
|
why = ("命中 ask 规则" if ask_by_rule else
|
|
169
136
|
"外部调用" if external else f"{side_effect} 副作用")
|
|
170
|
-
# 【P5-1】三个分支各自的结构化原因码
|
|
171
137
|
code = (ASK_RULE if ask_by_rule else
|
|
172
138
|
EXTERNAL_CONFIRM if external else SIDE_EFFECT_CONFIRM)
|
|
173
139
|
return PermissionDecision("ask", 4,
|
|
174
140
|
reason=f"需确认: {why}", trust_downgrade=downgrade,
|
|
175
141
|
requires_confirmation=True, reason_code=code)
|
|
176
142
|
|
|
177
|
-
# 放行:显式 allow 规则,或只读无副作用,或已确认
|
|
178
143
|
reason = ("命中 allow 规则" if allow_by_rule else
|
|
179
144
|
"只读无副作用" if read_only else "已确认")
|
|
180
145
|
return PermissionDecision("allow", 4, reason=reason, trust_downgrade=downgrade)
|