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
|
@@ -6,16 +6,17 @@ Foxclaw 行程子系统 — Itinerary Runtime
|
|
|
6
6
|
|
|
7
7
|
import csv
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import cast
|
|
9
10
|
|
|
10
|
-
from .models import
|
|
11
|
-
from .reasoning import ItineraryReasoning
|
|
11
|
+
from .models import ItineraryVerdict, TravelMode, Waypoint
|
|
12
|
+
from .reasoning import MODE_SPEED, ItineraryReasoning
|
|
12
13
|
from .amap_client import AMapClient
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class ItineraryRuntime:
|
|
16
17
|
"""行政 Agent 的行程内核。"""
|
|
17
18
|
|
|
18
|
-
def __init__(self, amap_key: str = None, hmr=None):
|
|
19
|
+
def __init__(self, amap_key: str | None = None, hmr=None):
|
|
19
20
|
self.waypoints: list = []
|
|
20
21
|
self.reasoning = ItineraryReasoning()
|
|
21
22
|
self.amap = AMapClient(amap_key)
|
|
@@ -25,13 +26,11 @@ class ItineraryRuntime:
|
|
|
25
26
|
def amap_enabled(self) -> bool:
|
|
26
27
|
return self.amap.enabled
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
lat: float = None, start_time: str = None,
|
|
29
|
+
def add_waypoint(self, name: str, address: str = "", lng: float | None = None,
|
|
30
|
+
lat: float | None = None, start_time: str | None = None,
|
|
31
31
|
duration_min: int = 0, city: str = "") -> dict:
|
|
32
32
|
wp = Waypoint(name=name, address=address, lng=lng, lat=lat,
|
|
33
33
|
start_time=start_time, duration_min=duration_min, city=city)
|
|
34
|
-
# 没坐标但有地址 → 尝试高德地理编码
|
|
35
34
|
if not wp.has_coord and address and self.amap_enabled:
|
|
36
35
|
geo = self.amap.geocode(address, city)
|
|
37
36
|
if geo:
|
|
@@ -62,7 +61,6 @@ class ItineraryRuntime:
|
|
|
62
61
|
count += 1
|
|
63
62
|
return count
|
|
64
63
|
|
|
65
|
-
# ── 真实路线(高德优先,否则估算)─────────────────────
|
|
66
64
|
def real_leg(self, a: Waypoint, b: Waypoint, mode: str = "driving"):
|
|
67
65
|
"""两点路线:高德可用且有坐标时用真实数据,否则直线估算。"""
|
|
68
66
|
if self.amap_enabled and a.has_coord and b.has_coord:
|
|
@@ -70,12 +68,12 @@ class ItineraryRuntime:
|
|
|
70
68
|
if r:
|
|
71
69
|
from .models import RouteLeg
|
|
72
70
|
return RouteLeg(from_id=a.id, to_id=b.id, from_name=a.name,
|
|
73
|
-
to_name=b.name,
|
|
71
|
+
to_name=b.name,
|
|
72
|
+
mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
|
|
74
73
|
distance_km=r["distance_km"],
|
|
75
74
|
duration_min=r["duration_min"], source="amap")
|
|
76
75
|
return self.reasoning.estimate_leg(a, b, mode)
|
|
77
76
|
|
|
78
|
-
# ── 综合分析(行政 Agent 核心输出)────────────────────
|
|
79
77
|
def plan(self, mode: str = "driving", optimize: bool = True) -> dict:
|
|
80
78
|
"""完整行程规划:优化顺序 + 冲突检测 + 聚类 + 守门人裁决。"""
|
|
81
79
|
opt = (self.reasoning.optimize_order(self.waypoints, mode)
|
|
@@ -95,7 +93,6 @@ class ItineraryRuntime:
|
|
|
95
93
|
"map_data": self._to_amap_mapdata(opt),
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
# ── 行程守门人 ────────────────────────────────────────
|
|
99
96
|
def _gatekeeper(self, conflicts, clusters, dens) -> list:
|
|
100
97
|
verdicts = []
|
|
101
98
|
for c in conflicts:
|
|
@@ -124,7 +121,6 @@ class ItineraryRuntime:
|
|
|
124
121
|
payload={"cluster": cl}))
|
|
125
122
|
return verdicts
|
|
126
123
|
|
|
127
|
-
# ── 导出高德可视化格式(MapTaskData)──────────────────
|
|
128
124
|
def _to_amap_mapdata(self, opt) -> list:
|
|
129
125
|
"""转成高德 MapTaskData(POI + route),可喂给高德生成可视化地图。"""
|
|
130
126
|
data = []
|
|
@@ -25,8 +25,8 @@ class CapabilityRegistry:
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
self._caps: dict[str, Any] = {}
|
|
27
27
|
self._lazy: dict[str, Callable[[], Any]] = {}
|
|
28
|
-
self._lock = threading.RLock()
|
|
29
|
-
self._build_locks: dict[str, threading.Lock] = {}
|
|
28
|
+
self._lock = threading.RLock()
|
|
29
|
+
self._build_locks: dict[str, threading.Lock] = {}
|
|
30
30
|
|
|
31
31
|
def register(self, name: str, handle: Any) -> None:
|
|
32
32
|
"""注册一个已构造好的能力实例(覆盖同名)。"""
|
|
@@ -38,7 +38,7 @@ class CapabilityRegistry:
|
|
|
38
38
|
with self._lock:
|
|
39
39
|
self._lazy[name] = factory
|
|
40
40
|
|
|
41
|
-
def get(self, name: str, default: Any = None) -> Any:
|
|
41
|
+
def get(self, name: str, default: Any | None = None) -> Any:
|
|
42
42
|
"""解析能力。已注册直接返回;惰性则首次构造并缓存;都没有返回 default。
|
|
43
43
|
|
|
44
44
|
【F1 修复】慢惰性构造(如 search_runtime 首建要装 HKC+载模型)**不持全局锁**,
|
|
@@ -54,12 +54,11 @@ class CapabilityRegistry:
|
|
|
54
54
|
blk = self._build_locks.get(name)
|
|
55
55
|
if blk is None:
|
|
56
56
|
blk = self._build_locks[name] = threading.Lock()
|
|
57
|
-
# 出全局锁,用 per-name 锁串行化"同一能力"的构造
|
|
58
57
|
with blk:
|
|
59
58
|
with self._lock:
|
|
60
|
-
if name in self._caps:
|
|
59
|
+
if name in self._caps:
|
|
61
60
|
return self._caps[name]
|
|
62
|
-
inst = factory()
|
|
61
|
+
inst = factory()
|
|
63
62
|
with self._lock:
|
|
64
63
|
self._caps[name] = inst
|
|
65
64
|
return inst
|
|
@@ -22,12 +22,10 @@ from ..core.governance_audit import new_decision_id
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class Verdict:
|
|
24
24
|
allowed: bool
|
|
25
|
-
level: str
|
|
25
|
+
level: str
|
|
26
26
|
reason: str = ""
|
|
27
|
-
source: str = ""
|
|
28
|
-
raw: Any = None
|
|
29
|
-
# 【P3-4】每次裁决的唯一 ID(P1-2 治理审计的关联键)。放最后+默认值:既有位置参数
|
|
30
|
-
# 构造(如 Verdict(True,"allow",...,"permission",None))不受影响。
|
|
27
|
+
source: str = ""
|
|
28
|
+
raw: Any = None
|
|
31
29
|
decision_id: str = field(default="")
|
|
32
30
|
|
|
33
31
|
|
|
@@ -35,9 +33,7 @@ class PolicyGate:
|
|
|
35
33
|
def __init__(self, spine=None, action_evaluator: Optional[Callable] = None,
|
|
36
34
|
auditor=None):
|
|
37
35
|
self._spine = spine
|
|
38
|
-
self._eval = action_evaluator
|
|
39
|
-
# 【P1-2】统一治理审计(可空:standalone 无审计)。鸭子类型注入,避免 kernel→core 之外
|
|
40
|
-
# 的装配环;core.governance_audit.GovernanceAuditor 是唯一预期实现。
|
|
36
|
+
self._eval = action_evaluator
|
|
41
37
|
self._auditor = auditor
|
|
42
38
|
|
|
43
39
|
@property
|
|
@@ -45,7 +41,6 @@ class PolicyGate:
|
|
|
45
41
|
"""SkillAdapter 经此取审计器,把执行终态关联到裁决 decision_id(P3-4 跨层关联)。"""
|
|
46
42
|
return self._auditor
|
|
47
43
|
|
|
48
|
-
# ── 推理准入(Harness)──────────────────────────────────────
|
|
49
44
|
def check_reasoning(self, harness, node) -> Verdict:
|
|
50
45
|
"""经统一门做推理漂移准入。底层仍是那一次 harness.check(同一 HarnessResult),
|
|
51
46
|
Harness 自身已 emit harness_block/warn 事件,门面**不重复 emit**。行为一致。
|
|
@@ -64,7 +59,6 @@ class PolicyGate:
|
|
|
64
59
|
"dominant_dim": getattr(r, "dominant_dim", "")})
|
|
65
60
|
return v
|
|
66
61
|
|
|
67
|
-
# ── 动作准入(skill-guard)──────────────────────────────────
|
|
68
62
|
def check_action(self, skill, args: dict, rules=None,
|
|
69
63
|
confirmed: bool = False, mode: str = "strict",
|
|
70
64
|
taint_level: int = 0, taint_threshold: int = 0) -> Verdict:
|
|
@@ -16,16 +16,16 @@ import threading
|
|
|
16
16
|
import time
|
|
17
17
|
import uuid
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Optional
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
# 合法状态与允许的流转(状态机约束:防止非法跳转)
|
|
22
22
|
_VALID_TRANSITIONS = {
|
|
23
23
|
"created": {"queued", "failed"},
|
|
24
24
|
"queued": {"scheduled", "failed"},
|
|
25
25
|
"scheduled": {"running", "failed"},
|
|
26
26
|
"running": {"completed", "failed"},
|
|
27
|
-
"completed": set(),
|
|
28
|
-
"failed": set(),
|
|
27
|
+
"completed": set(),
|
|
28
|
+
"failed": set(),
|
|
29
29
|
}
|
|
30
30
|
_TERMINAL = {"completed", "failed"}
|
|
31
31
|
|
|
@@ -33,15 +33,15 @@ _TERMINAL = {"completed", "failed"}
|
|
|
33
33
|
@dataclass
|
|
34
34
|
class ProcessRecord:
|
|
35
35
|
task: str
|
|
36
|
-
agent_id: str = None
|
|
36
|
+
agent_id: Optional[str] = None
|
|
37
37
|
state: str = "created"
|
|
38
38
|
process_id: str = field(default_factory=lambda: f"proc_{uuid.uuid4().hex[:8]}")
|
|
39
39
|
priority: float = 0.0
|
|
40
|
-
route_hint: str = None
|
|
40
|
+
route_hint: Optional[str] = None
|
|
41
41
|
created_at: float = field(default_factory=time.time)
|
|
42
42
|
updated_at: float = field(default_factory=time.time)
|
|
43
43
|
state_history: list = field(default_factory=list)
|
|
44
|
-
fail_reason: str = None
|
|
44
|
+
fail_reason: Optional[str] = None
|
|
45
45
|
|
|
46
46
|
def to_dict(self) -> dict:
|
|
47
47
|
return {
|
|
@@ -58,21 +58,13 @@ class ProcessManager:
|
|
|
58
58
|
|
|
59
59
|
def __init__(self, bus=None, history_limit: int = 1000):
|
|
60
60
|
self._processes: dict[str, ProcessRecord] = {}
|
|
61
|
-
self.bus = bus
|
|
61
|
+
self.bus = bus
|
|
62
62
|
self._history_limit = history_limit
|
|
63
|
-
# 【深审优化】活跃(非终态)进程数的 O(1) 增量计数:create 时 +1、进入终态时 -1
|
|
64
|
-
# (集中在 _transition)。供 CognitiveState 每次 lifecycle 事件 O(1) 取用,
|
|
65
|
-
# 免去原先每事件 stats() 遍历全部进程(O(n),n≤1000)。
|
|
66
63
|
self._active = 0
|
|
67
|
-
# 并行子 Agent 会让多个线程并发 create/mark_*/_evict 同一个 _processes dict,
|
|
68
|
-
# 且状态机流转是"读 state → 校验 → 写 state"的非原子序列。RLock 保证:
|
|
69
|
-
# · dict 增删与遍历(list/stats/_evict)不撞"dict changed size";
|
|
70
|
-
# · 同一进程的并发二次流转不会都通过终态校验后重复 transition。
|
|
71
|
-
# 可重入:_emit→bus.publish 的处理器可能回调 pm 的方法(同线程重入安全)。
|
|
72
64
|
self._lock = threading.RLock()
|
|
73
65
|
|
|
74
|
-
def create(self, task: str, agent_id: str = None,
|
|
75
|
-
schedule: dict = None) -> ProcessRecord:
|
|
66
|
+
def create(self, task: str, agent_id: str | None = None,
|
|
67
|
+
schedule: dict | None = None) -> ProcessRecord:
|
|
76
68
|
"""创建进程(任务进入系统)。携带调度决策。"""
|
|
77
69
|
rec = ProcessRecord(
|
|
78
70
|
task=task, agent_id=agent_id,
|
|
@@ -81,36 +73,33 @@ class ProcessManager:
|
|
|
81
73
|
)
|
|
82
74
|
with self._lock:
|
|
83
75
|
rec.state_history.append("created")
|
|
84
|
-
# 创建后立即入队 → 已调度(调度决策此时已有)
|
|
85
76
|
self._transition(rec, "queued")
|
|
86
77
|
if schedule:
|
|
87
78
|
self._transition(rec, "scheduled")
|
|
88
79
|
self._processes[rec.process_id] = rec
|
|
89
|
-
self._active += 1
|
|
80
|
+
self._active += 1
|
|
90
81
|
self._evict_if_needed()
|
|
91
82
|
self._emit(rec)
|
|
92
83
|
return rec
|
|
93
84
|
|
|
94
|
-
def mark_running(self, process_id: str) -> ProcessRecord:
|
|
85
|
+
def mark_running(self, process_id: str) -> Optional[ProcessRecord]:
|
|
95
86
|
with self._lock:
|
|
96
87
|
rec = self._processes.get(process_id)
|
|
97
88
|
if rec is None:
|
|
98
89
|
return None
|
|
99
|
-
# 若仍在 queued(create 未带 schedule),先补 scheduled 再 running
|
|
100
90
|
if rec.state == "queued":
|
|
101
91
|
self._transition(rec, "scheduled")
|
|
102
92
|
return self._safe_transition(process_id, "running")
|
|
103
93
|
|
|
104
|
-
def mark_completed(self, process_id: str) -> ProcessRecord:
|
|
94
|
+
def mark_completed(self, process_id: str) -> Optional[ProcessRecord]:
|
|
105
95
|
with self._lock:
|
|
106
96
|
return self._safe_transition(process_id, "completed")
|
|
107
97
|
|
|
108
|
-
def mark_failed(self, process_id: str, reason: str = "") -> ProcessRecord:
|
|
98
|
+
def mark_failed(self, process_id: str, reason: str = "") -> Optional[ProcessRecord]:
|
|
109
99
|
with self._lock:
|
|
110
100
|
rec = self._processes.get(process_id)
|
|
111
101
|
if rec is None:
|
|
112
102
|
return None
|
|
113
|
-
# 已是终态 → 幂等忽略(重试/并发下的二次标记不算错误,不崩溃)
|
|
114
103
|
if rec.state in _TERMINAL:
|
|
115
104
|
return rec
|
|
116
105
|
self._transition(rec, "failed")
|
|
@@ -118,11 +107,11 @@ class ProcessManager:
|
|
|
118
107
|
self._emit(rec)
|
|
119
108
|
return rec
|
|
120
109
|
|
|
121
|
-
def get(self, process_id: str) -> ProcessRecord:
|
|
110
|
+
def get(self, process_id: str) -> Optional[ProcessRecord]:
|
|
122
111
|
with self._lock:
|
|
123
112
|
return self._processes.get(process_id)
|
|
124
113
|
|
|
125
|
-
def list(self, state: str = None, limit: int = 50) -> list:
|
|
114
|
+
def list(self, state: str | None = None, limit: int = 50) -> list:
|
|
126
115
|
with self._lock:
|
|
127
116
|
items = list(self._processes.values())
|
|
128
117
|
if state:
|
|
@@ -138,21 +127,18 @@ class ProcessManager:
|
|
|
138
127
|
|
|
139
128
|
def stats(self) -> dict:
|
|
140
129
|
with self._lock:
|
|
141
|
-
counts = {}
|
|
130
|
+
counts: dict[str, int] = {}
|
|
142
131
|
for r in self._processes.values():
|
|
143
132
|
counts[r.state] = counts.get(r.state, 0) + 1
|
|
144
133
|
return {"total": len(self._processes), "by_state": counts,
|
|
145
134
|
"active": self._active}
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
def _safe_transition(self, process_id: str, next_state: str) -> ProcessRecord:
|
|
136
|
+
def _safe_transition(self, process_id: str, next_state: str) -> Optional[ProcessRecord]:
|
|
149
137
|
rec = self._processes.get(process_id)
|
|
150
138
|
if rec is None:
|
|
151
139
|
return None
|
|
152
|
-
# 已是终态 → 幂等忽略(不崩溃)
|
|
153
140
|
if rec.state in _TERMINAL:
|
|
154
141
|
return rec
|
|
155
|
-
# 已在目标状态 → 幂等忽略(重复标记不报错)
|
|
156
142
|
if rec.state == next_state:
|
|
157
143
|
return rec
|
|
158
144
|
self._transition(rec, next_state)
|
|
@@ -167,15 +153,12 @@ class ProcessManager:
|
|
|
167
153
|
新增调用点直接调本方法,活跃计数会被重复扣减(可扣成负数,stats 失真)。
|
|
168
154
|
这里是双保险。注意行为变更:终态 → 任何状态,从抛 ValueError 变为静默忽略。
|
|
169
155
|
"""
|
|
170
|
-
# 已是终态 → 幂等忽略(不再 -1,不再改状态)
|
|
171
156
|
if rec.state in _TERMINAL:
|
|
172
157
|
return
|
|
173
158
|
allowed = _VALID_TRANSITIONS.get(rec.state, set())
|
|
174
159
|
if next_state not in allowed:
|
|
175
160
|
raise ValueError(
|
|
176
161
|
f"非法状态流转: {rec.state} → {next_state}(进程 {rec.process_id})")
|
|
177
|
-
# 进入终态 → 活跃计数 -1(每进程恰好 create 时 +1、首次入终态时 -1;
|
|
178
|
-
# 上面的终态守卫保证不会重复扣减)。
|
|
179
162
|
if next_state in _TERMINAL:
|
|
180
163
|
self._active -= 1
|
|
181
164
|
rec.state = next_state
|
|
@@ -183,8 +166,6 @@ class ProcessManager:
|
|
|
183
166
|
rec.state_history.append(next_state)
|
|
184
167
|
|
|
185
168
|
def _emit(self, rec: ProcessRecord):
|
|
186
|
-
# 【v2 ① Step3·Lifecycle 统一】emit 唯一生命周期词汇 task.lifecycle.<state>
|
|
187
|
-
# (取代旧 process.state_changed;Scheduler 委托本 pm,不再自发另一套词汇)。
|
|
188
169
|
if self.bus is not None:
|
|
189
170
|
try:
|
|
190
171
|
self.bus.publish(f"task.lifecycle.{rec.state}", {
|
|
@@ -29,7 +29,7 @@ class _PriorityAdmission:
|
|
|
29
29
|
self.max = max_slots
|
|
30
30
|
self._free = max_slots or 0
|
|
31
31
|
self._cv = threading.Condition()
|
|
32
|
-
self._heap: list = []
|
|
32
|
+
self._heap: list = []
|
|
33
33
|
self._seq = 0
|
|
34
34
|
|
|
35
35
|
def acquire(self, priority: int = 0) -> None:
|
|
@@ -43,7 +43,7 @@ class _PriorityAdmission:
|
|
|
43
43
|
self._cv.wait()
|
|
44
44
|
heapq.heappop(self._heap)
|
|
45
45
|
self._free -= 1
|
|
46
|
-
if self._free > 0 and self._heap:
|
|
46
|
+
if self._free > 0 and self._heap:
|
|
47
47
|
self._cv.notify_all()
|
|
48
48
|
|
|
49
49
|
def release(self) -> None:
|
|
@@ -58,11 +58,8 @@ class Scheduler:
|
|
|
58
58
|
def __init__(self, max_concurrency: Optional[int] = None, spine=None,
|
|
59
59
|
process_manager=None):
|
|
60
60
|
self.max_concurrency = max_concurrency if (max_concurrency and max_concurrency > 0) else None
|
|
61
|
-
# Step5:优先级准入(满载时高优先排队者先获空位;不抢占正在跑的)。无上限时零等待。
|
|
62
61
|
self._adm = _PriorityAdmission(self.max_concurrency)
|
|
63
62
|
self._spine = spine
|
|
64
|
-
# 【Step3】统一生命周期:有 ProcessManager 则委托它发 task.lifecycle.*(唯一词汇、
|
|
65
|
-
# 唯一状态机);无 pm(standalone/测试)则调度器自发 task.lifecycle.* 作回退。
|
|
66
63
|
self.process_manager = process_manager
|
|
67
64
|
self._lock = threading.Lock()
|
|
68
65
|
self._stats = {"submitted": 0, "running": 0, "completed": 0,
|
|
@@ -80,7 +77,6 @@ class Scheduler:
|
|
|
80
77
|
proc = pm.create(task, agent_id=agent_id, schedule={"priority": priority})
|
|
81
78
|
else:
|
|
82
79
|
self._emit("scheduled", agent_id, task, priority)
|
|
83
|
-
# 优先级准入:满载则排队等空位(高优先先入);无上限时零等待(behavior-preserving)
|
|
84
80
|
self._adm.acquire(priority)
|
|
85
81
|
try:
|
|
86
82
|
with self._lock:
|
|
@@ -90,8 +86,6 @@ class Scheduler:
|
|
|
90
86
|
if proc is not None:
|
|
91
87
|
pm.mark_running(proc.process_id)
|
|
92
88
|
else:
|
|
93
|
-
# 【P5-2】回退路径的词汇与 ProcessManager 对齐:started → running
|
|
94
|
-
# (pm 的词汇是 created/queued/scheduled/running/completed/failed)
|
|
95
89
|
self._emit("running", agent_id, task, priority)
|
|
96
90
|
result = fn()
|
|
97
91
|
with self._lock:
|
|
@@ -116,7 +110,7 @@ class Scheduler:
|
|
|
116
110
|
|
|
117
111
|
def stats(self) -> dict:
|
|
118
112
|
with self._lock:
|
|
119
|
-
s = dict(self._stats)
|
|
113
|
+
s: dict[str, int | None] = dict(self._stats)
|
|
120
114
|
s["max_concurrency"] = self.max_concurrency
|
|
121
115
|
return s
|
|
122
116
|
|
|
@@ -23,12 +23,9 @@ import secrets
|
|
|
23
23
|
import sys
|
|
24
24
|
import time
|
|
25
25
|
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
# ── 凭证保险箱(vault)────────────────────────────────────────
|
|
29
|
-
# 真实凭证只进守门人进程,主进程内存里从不出现。两个来源:
|
|
30
|
-
# 1. 环境变量前缀 FOXCLAW_GUARDIAN_SECRET_<REF>(REF 统一小写);
|
|
31
|
-
# 2. FOXCLAW_GUARDIAN_SECRETS 指向的 JSON 文件 {"ref": "secret", ...}。
|
|
32
29
|
def load_vault() -> dict:
|
|
33
30
|
vault: dict = {}
|
|
34
31
|
prefix = "FOXCLAW_GUARDIAN_SECRET_"
|
|
@@ -42,13 +39,10 @@ def load_vault() -> dict:
|
|
|
42
39
|
if isinstance(d, dict):
|
|
43
40
|
vault.update({str(k).lower(): v for k, v in d.items()})
|
|
44
41
|
except Exception:
|
|
45
|
-
pass
|
|
42
|
+
pass
|
|
46
43
|
return vault
|
|
47
44
|
|
|
48
45
|
|
|
49
|
-
# ── MCP server 配置(守门人持有凭证)──────────────────────────
|
|
50
|
-
# 与 vault 对称:MCP 的 headers 里可能含真凭证(Authorization/x-api-key 等)。
|
|
51
|
-
# 主进程通过 mcp_register 推送配置;也可用 FOXCLAW_GUARDIAN_MCP_SERVERS 指向 JSON 文件做初始加载。
|
|
52
46
|
def _mcp_servers_path() -> str:
|
|
53
47
|
"""守门人侧 MCP 配置持久化文件。优先 FOXCLAW_GUARDIAN_MCP_SERVERS 显式指定;
|
|
54
48
|
否则用 FOXCLAW_GUARDIAN_DATA_DIR 下的 mcp_guardian_servers.json。主进程的 mcp_servers.json 已不含 headers。"""
|
|
@@ -79,7 +73,7 @@ def load_mcp_servers() -> dict:
|
|
|
79
73
|
"transport": c.get("transport") or "streamable_http",
|
|
80
74
|
}
|
|
81
75
|
except Exception:
|
|
82
|
-
pass
|
|
76
|
+
pass
|
|
83
77
|
return servers
|
|
84
78
|
|
|
85
79
|
|
|
@@ -94,18 +88,15 @@ def _persist_mcp_servers(servers: dict) -> None:
|
|
|
94
88
|
tmp.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
95
89
|
tmp.replace(Path(path))
|
|
96
90
|
except Exception:
|
|
97
|
-
pass
|
|
91
|
+
pass
|
|
98
92
|
|
|
99
93
|
|
|
100
|
-
# ── 短期可撤销能力令牌 ──────────────────────────────────────
|
|
101
|
-
# 彻底取代「resolve 真值借出」:主进程只拿得到一个不透明随机 token,真值永不出守门人进程。
|
|
102
|
-
# 令牌 = 一次性 + 过期 + 可撤销,进程内状态表(不依赖 HMAC 签名,因为 token 本身高熵、无法伪造/猜中)。
|
|
103
94
|
class TokenStore:
|
|
104
95
|
def __init__(self, default_ttl: float = 60.0):
|
|
105
96
|
self._default_ttl = default_ttl
|
|
106
97
|
self._tokens: dict = {}
|
|
107
98
|
|
|
108
|
-
def mint(self, ref: str, ttl: float = None) -> dict:
|
|
99
|
+
def mint(self, ref: str, ttl: float | None = None) -> dict:
|
|
109
100
|
"""为某 ref 签发能力令牌。返回 {token, expires_in},绝不返回真值。"""
|
|
110
101
|
ttl = ttl if ttl is not None else self._default_ttl
|
|
111
102
|
token = secrets.token_urlsafe(32)
|
|
@@ -137,7 +128,6 @@ class TokenStore:
|
|
|
137
128
|
return True
|
|
138
129
|
|
|
139
130
|
|
|
140
|
-
# ── 跨进程重建(守门人侧)─────────────────────────────────────
|
|
141
131
|
def _rebuild_skill(sd: dict):
|
|
142
132
|
"""主进程只传 evaluate 需要的字段 + 已算好的 trust。重建为鸭子类型对象。"""
|
|
143
133
|
from types import SimpleNamespace
|
|
@@ -145,7 +135,7 @@ def _rebuild_skill(sd: dict):
|
|
|
145
135
|
return SimpleNamespace(
|
|
146
136
|
id=sd.get("id", ""),
|
|
147
137
|
kind=sd.get("kind", "local"),
|
|
148
|
-
side_effect=sd.get("side_effect", "write"),
|
|
138
|
+
side_effect=sd.get("side_effect", "write"),
|
|
149
139
|
destructive=bool(sd.get("destructive", False)),
|
|
150
140
|
external=bool(sd.get("external", False)),
|
|
151
141
|
read_only=sd.get("read_only", None),
|
|
@@ -187,7 +177,7 @@ def _handle_mint(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
187
177
|
ttl = float(ttl) if ttl else None
|
|
188
178
|
out = store.mint(ref, ttl)
|
|
189
179
|
print(f"[Sentinel] mint {ref}", file=sys.stderr, flush=True)
|
|
190
|
-
return out
|
|
180
|
+
return out
|
|
191
181
|
|
|
192
182
|
|
|
193
183
|
def _handle_revoke(msg: dict, store: TokenStore) -> dict:
|
|
@@ -212,7 +202,7 @@ def _handle_mcp_register(msg: dict, servers: dict) -> dict:
|
|
|
212
202
|
headers = dict(c.get("headers") or {})
|
|
213
203
|
old = servers.get(sid)
|
|
214
204
|
if not headers and old:
|
|
215
|
-
headers = dict(old.get("headers") or {})
|
|
205
|
+
headers = dict(old.get("headers") or {})
|
|
216
206
|
new_servers[sid] = {
|
|
217
207
|
"name": c.get("name") or sid,
|
|
218
208
|
"url": c.get("url", ""),
|
|
@@ -251,7 +241,7 @@ def _handle_mcp_call(msg: dict, servers: dict) -> dict:
|
|
|
251
241
|
if out.get("isError"):
|
|
252
242
|
return SkillResult(skill_id, False, status="blocked",
|
|
253
243
|
message=f"MCP 工具返回错误: {out.get('content') or 'unknown'}").to_dict()
|
|
254
|
-
out = _mark_untrusted(out)
|
|
244
|
+
out = _mark_untrusted(out)
|
|
255
245
|
print(f"[Sentinel] mcp_call {server_id}.{tool}", file=sys.stderr, flush=True)
|
|
256
246
|
return SkillResult(skill_id, True, output=out,
|
|
257
247
|
message="MCP 调用完成(外部不可信数据)").to_dict()
|
|
@@ -329,7 +319,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
329
319
|
secret = None
|
|
330
320
|
used_ref = None
|
|
331
321
|
if token:
|
|
332
|
-
ref = store.redeem(token)
|
|
322
|
+
ref = store.redeem(token)
|
|
333
323
|
if ref is None:
|
|
334
324
|
return SkillResult(skill_id, False, status="blocked",
|
|
335
325
|
message="能力令牌无效/已过期/已使用/已撤销").to_dict()
|
|
@@ -345,7 +335,6 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
345
335
|
return SkillResult(skill_id, False, status="blocked",
|
|
346
336
|
message=f"凭证 ref「{secret_ref}」未在守门人 vault 中配置").to_dict()
|
|
347
337
|
|
|
348
|
-
# SSRF 校验:守门人侧也做(主进程可能被攻破传内网地址,不能只信主进程)
|
|
349
338
|
block = ssrf_check(endpoint, allow_local=allow_local)
|
|
350
339
|
if block:
|
|
351
340
|
return SkillResult(skill_id, False, status="blocked",
|
|
@@ -364,7 +353,7 @@ def _handle_execute_web(msg: dict, vault: dict, store: TokenStore) -> dict:
|
|
|
364
353
|
return SkillResult(skill_id, False, status="blocked",
|
|
365
354
|
message="缺少 httpx 依赖,无法发起 Web API 调用").to_dict()
|
|
366
355
|
|
|
367
|
-
req_kwargs = {"headers": headers, "timeout": timeout, "follow_redirects": False}
|
|
356
|
+
req_kwargs: dict[str, Any] = {"headers": headers, "timeout": timeout, "follow_redirects": False}
|
|
368
357
|
if loc == "query":
|
|
369
358
|
req_kwargs["params"] = args
|
|
370
359
|
else:
|
|
@@ -416,6 +405,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
|
|
|
416
405
|
mid = msg.get("id")
|
|
417
406
|
op = msg.get("op")
|
|
418
407
|
try:
|
|
408
|
+
result: dict[str, object]
|
|
419
409
|
if op == "ping":
|
|
420
410
|
result = {"pong": True}
|
|
421
411
|
elif op == "evaluate":
|
|
@@ -438,7 +428,7 @@ def serve_stdio(vault: dict, store: TokenStore, servers: dict) -> None:
|
|
|
438
428
|
else:
|
|
439
429
|
result = {"error": f"unknown op: {op}"}
|
|
440
430
|
_reply({"id": mid, "ok": True, "result": result})
|
|
441
|
-
except Exception as e:
|
|
431
|
+
except Exception as e:
|
|
442
432
|
_reply({"id": mid, "ok": False, "error": str(e)})
|
|
443
433
|
|
|
444
434
|
|
|
@@ -22,20 +22,18 @@ class SentinelUnavailable(RuntimeError):
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _project_root() -> str:
|
|
25
|
-
# foxclaw/kernel/sentinel_client.py → parents[0]=kernel, [1]=foxclaw, [2]=Foxclaw
|
|
26
25
|
return str(Path(__file__).resolve().parents[2])
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class SentinelClient:
|
|
30
|
-
def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str = None):
|
|
29
|
+
def __init__(self, timeout: float = 5.0, cmd=None, data_dir: str | None = None):
|
|
31
30
|
self._timeout = timeout
|
|
32
31
|
self._data_dir = data_dir
|
|
33
32
|
self._lock = threading.Lock()
|
|
34
33
|
self._req_id = 0
|
|
35
|
-
self._proc = None
|
|
34
|
+
self._proc: subprocess.Popen[str] | None = None
|
|
36
35
|
self._spawn(cmd)
|
|
37
36
|
|
|
38
|
-
# ── 进程生命周期 ─────────────────────────────────────────
|
|
39
37
|
def _spawn(self, cmd) -> None:
|
|
40
38
|
if cmd is None:
|
|
41
39
|
cmd = [sys.executable, "-m", "foxclaw.kernel.sentinel"]
|
|
@@ -50,7 +48,7 @@ class SentinelClient:
|
|
|
50
48
|
except Exception as e:
|
|
51
49
|
raise RuntimeError(f"Sentinel 启动失败: {e}")
|
|
52
50
|
try:
|
|
53
|
-
self._request("ping", {})
|
|
51
|
+
self._request("ping", {})
|
|
54
52
|
except Exception as e:
|
|
55
53
|
self._kill()
|
|
56
54
|
raise RuntimeError(f"Sentinel 握手失败: {e}")
|
|
@@ -73,16 +71,18 @@ class SentinelClient:
|
|
|
73
71
|
except Exception:
|
|
74
72
|
pass
|
|
75
73
|
|
|
76
|
-
# ── IPC 核心 ─────────────────────────────────────────────
|
|
77
74
|
def _request(self, op: str, payload: dict):
|
|
78
75
|
with self._lock:
|
|
79
76
|
self._req_id += 1
|
|
80
77
|
mid = self._req_id
|
|
81
78
|
msg = json.dumps({"id": mid, "op": op, **payload}, ensure_ascii=False)
|
|
82
79
|
try:
|
|
83
|
-
self._proc
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
proc = self._proc
|
|
81
|
+
if proc is None or proc.stdin is None or proc.stdout is None:
|
|
82
|
+
raise SentinelUnavailable("守门人进程未启动")
|
|
83
|
+
proc.stdin.write(msg + "\n")
|
|
84
|
+
proc.stdin.flush()
|
|
85
|
+
line = proc.stdout.readline()
|
|
86
86
|
except Exception as e:
|
|
87
87
|
raise SentinelUnavailable(f"IPC 通信失败: {e}")
|
|
88
88
|
if not line:
|
|
@@ -95,7 +95,6 @@ class SentinelClient:
|
|
|
95
95
|
raise SentinelUnavailable(resp.get("error", "unknown error"))
|
|
96
96
|
return resp.get("result")
|
|
97
97
|
|
|
98
|
-
# ── 序列化(只传 evaluate 需要的字段,不含 handler/凭证)────
|
|
99
98
|
@staticmethod
|
|
100
99
|
def _serialize_skill(skill) -> dict:
|
|
101
100
|
_tl = getattr(skill, "trust_level", None)
|
|
@@ -106,7 +105,7 @@ class SentinelClient:
|
|
|
106
105
|
return {
|
|
107
106
|
"id": getattr(skill, "id", ""),
|
|
108
107
|
"kind": getattr(skill, "kind", "local"),
|
|
109
|
-
"side_effect": getattr(skill, "side_effect", "write"),
|
|
108
|
+
"side_effect": getattr(skill, "side_effect", "write"),
|
|
110
109
|
"destructive": bool(getattr(skill, "destructive", False)),
|
|
111
110
|
"external": bool(getattr(skill, "external", False)),
|
|
112
111
|
"read_only": getattr(skill, "read_only", None),
|
|
@@ -124,7 +123,6 @@ class SentinelClient:
|
|
|
124
123
|
"allow": list(getattr(rules, "allow", []) or []),
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
# ── 对外接口 ─────────────────────────────────────────────
|
|
128
126
|
def evaluate(self, skill, args, rules=None, confirmed=False, mode="strict",
|
|
129
127
|
taint_level=0, taint_threshold=0):
|
|
130
128
|
"""动作裁决(跨进程)。签名对齐 skills.permission.evaluate,返回 PermissionDecision。"""
|
|
@@ -141,7 +139,6 @@ class SentinelClient:
|
|
|
141
139
|
try:
|
|
142
140
|
d = self._request("evaluate", payload)
|
|
143
141
|
except SentinelUnavailable as e:
|
|
144
|
-
# fail-closed:守门人不可达 → deny,绝不降级放行
|
|
145
142
|
return PermissionDecision("deny", 0,
|
|
146
143
|
reason=f"守门人不可达(fail-closed): {e}",
|
|
147
144
|
reason_code="SENTINEL_UNAVAILABLE")
|
|
@@ -154,9 +151,9 @@ class SentinelClient:
|
|
|
154
151
|
reason_code=d.get("reason_code", ""),
|
|
155
152
|
)
|
|
156
153
|
|
|
157
|
-
def mint(self, ref: str, ttl: float = None):
|
|
154
|
+
def mint(self, ref: str, ttl: float | None = None):
|
|
158
155
|
"""为 ref 签发一次性能力令牌。返回 token(绝不返回真值);失败返回 None。"""
|
|
159
|
-
payload = {"ref": ref}
|
|
156
|
+
payload: dict[str, object] = {"ref": ref}
|
|
160
157
|
if ttl is not None:
|
|
161
158
|
payload["ttl"] = ttl
|
|
162
159
|
try:
|
|
@@ -32,7 +32,6 @@ class Event:
|
|
|
32
32
|
source: str = ""
|
|
33
33
|
correlation_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
34
34
|
|
|
35
|
-
# ── 与旧 BusMessage 同形,便于 SystemBus 无痛门面化 ──
|
|
36
35
|
@property
|
|
37
36
|
def channel(self) -> str:
|
|
38
37
|
return self.topic
|
|
@@ -46,8 +45,8 @@ class EventSpine:
|
|
|
46
45
|
"""进程内唯一事件脊柱。发布/订阅(前缀匹配) + 投影 + 有界历史。线程安全。"""
|
|
47
46
|
|
|
48
47
|
def __init__(self, history_limit: int = 1000):
|
|
49
|
-
self._subs: list[tuple[str, Callable]] = []
|
|
50
|
-
self._projections: list[Callable] = []
|
|
48
|
+
self._subs: list[tuple[str, Callable]] = []
|
|
49
|
+
self._projections: list[Callable] = []
|
|
51
50
|
self._history: deque = deque(maxlen=history_limit)
|
|
52
51
|
self._lock = threading.RLock()
|
|
53
52
|
|
|
@@ -61,13 +60,15 @@ class EventSpine:
|
|
|
61
60
|
with self._lock:
|
|
62
61
|
self._projections.append(sink)
|
|
63
62
|
|
|
64
|
-
def publish(self, topic: str, payload: dict = None, source: str = "",
|
|
65
|
-
correlation_id: str = None) -> Event:
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
def publish(self, topic: str, payload: dict | None = None, source: str = "",
|
|
64
|
+
correlation_id: str | None = None) -> Event:
|
|
65
|
+
if correlation_id:
|
|
66
|
+
evt = Event(topic=topic, payload=payload or {}, source=source,
|
|
67
|
+
correlation_id=correlation_id)
|
|
68
|
+
else:
|
|
69
|
+
evt = Event(topic=topic, payload=payload or {}, source=source)
|
|
68
70
|
with self._lock:
|
|
69
71
|
self._history.append(evt)
|
|
70
|
-
# 锁内只做"快照",handler/sink 在锁外执行(避免与 pm/hmr 锁形成环)
|
|
71
72
|
handlers = [h for pfx, h in self._subs if topic.startswith(pfx)]
|
|
72
73
|
projections = list(self._projections)
|
|
73
74
|
for h in handlers:
|
|
@@ -90,7 +91,6 @@ class EventSpine:
|
|
|
90
91
|
|
|
91
92
|
def stats(self) -> dict:
|
|
92
93
|
with self._lock:
|
|
93
|
-
# 【P4-2】补 history_limit(deque.maxlen):只报 history_size 无法判断「是否已满」。
|
|
94
94
|
return {"subscriptions": len(self._subs),
|
|
95
95
|
"projections": len(self._projections),
|
|
96
96
|
"history_size": len(self._history),
|