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
|
@@ -23,36 +23,30 @@ def check(cond, msg):
|
|
|
23
23
|
def test_loop_detection():
|
|
24
24
|
print("\n【1】工具循环检测")
|
|
25
25
|
from foxclaw.harness.loop_detection import ToolLoopDetector
|
|
26
|
-
# repeat
|
|
27
26
|
d = ToolLoopDetector(warning=3, critical=5, circuit_break=8)
|
|
28
27
|
for _ in range(8):
|
|
29
28
|
v = d.record("web_search", {"q": "天气"})
|
|
30
29
|
check(v.stuck and v.detector == "repeat", "重复调用检测为 repeat")
|
|
31
|
-
# ping_pong
|
|
32
30
|
d2 = ToolLoopDetector(warning=4, critical=6, circuit_break=10)
|
|
33
31
|
for i in range(6):
|
|
34
32
|
v2 = d2.record("search" if i % 2 == 0 else "fetch", {"x": i % 2})
|
|
35
33
|
check(v2.stuck and v2.detector == "ping_pong", "交替调用检测为 ping_pong")
|
|
36
|
-
# no_progress
|
|
37
34
|
d3 = ToolLoopDetector(warning=3, critical=5, circuit_break=8)
|
|
38
35
|
for _ in range(5):
|
|
39
36
|
v3 = d3.record("poll", {"id": "j"}, result={"status": "pending"})
|
|
40
37
|
check(v3.stuck and v3.detector == "no_progress", "轮询无变化检测为 no_progress")
|
|
41
|
-
# 正常不误报
|
|
42
38
|
d4 = ToolLoopDetector(warning=3)
|
|
43
39
|
for q in ["a", "b", "c", "d", "e"]:
|
|
44
40
|
v4 = d4.record("search", {"q": q})
|
|
45
41
|
check(not v4.stuck, "多样调用不误报")
|
|
46
|
-
# 熔断
|
|
47
42
|
check(d.should_abort(), "达阈值 should_abort")
|
|
48
43
|
|
|
49
44
|
|
|
50
45
|
def test_loop_per_session():
|
|
51
46
|
print("\n【1b】循环检测 per-session 隔离(阶段四·步骤13,消除跨会话污染)")
|
|
52
47
|
from foxclaw.skills import SkillAdapter, set_loop_session, reset_loop_session, _loop_session
|
|
53
|
-
adp = SkillAdapter(None)
|
|
48
|
+
adp = SkillAdapter(None)
|
|
54
49
|
|
|
55
|
-
# 会话 A 连续同工具 8 次(达熔断)
|
|
56
50
|
tok = set_loop_session("sess_A")
|
|
57
51
|
try:
|
|
58
52
|
for _ in range(8):
|
|
@@ -61,7 +55,6 @@ def test_loop_per_session():
|
|
|
61
55
|
reset_loop_session(tok)
|
|
62
56
|
check(va.stuck, "会话 A 连续同工具达熔断")
|
|
63
57
|
|
|
64
|
-
# 切到会话 B:历史应从零,同工具仅 1 次不应被 A 污染成 stuck
|
|
65
58
|
tok = set_loop_session("sess_B")
|
|
66
59
|
try:
|
|
67
60
|
vb = adp._detector_for(_loop_session.get()).record("web_search", {"q": "天气"})
|
|
@@ -70,7 +63,6 @@ def test_loop_per_session():
|
|
|
70
63
|
check(not vb.stuck, "会话 B 不受会话 A 污染(独立历史)")
|
|
71
64
|
check(not adp._detector_for("sess_B").should_abort(), "会话 B 未达熔断")
|
|
72
65
|
|
|
73
|
-
# 不同会话持有独立检测器实例;无会话时用共享默认槽(向后兼容)
|
|
74
66
|
check(adp._detector_for("sess_A") is not adp._detector_for("sess_B"), "不同会话独立检测器实例")
|
|
75
67
|
check(adp._detector_for(None) is adp._detector_for(None), "无会话共用默认槽(稳定)")
|
|
76
68
|
check(_loop_session.get() is None, "退出后 contextvar 已复位(无残留)")
|
|
@@ -79,12 +71,9 @@ def test_loop_per_session():
|
|
|
79
71
|
def test_compaction():
|
|
80
72
|
print("\n【2】上下文压缩")
|
|
81
73
|
from foxclaw.core.compaction import Compactor, extract_identifiers, estimate_messages_tokens
|
|
82
|
-
# token 估算(压缩触发的判据)
|
|
83
74
|
check(estimate_messages_tokens([{"role": "user", "content": "你好世界"}]) > 0, "token 估算非零")
|
|
84
|
-
# 标识符提取(含 ¥金额)
|
|
85
75
|
ids = extract_identifiers([{"role": "user", "content": "订单 txn_abc12345 金额 ¥1,200 流水 202606210001"}])
|
|
86
76
|
check("txn_abc12345" in ids and any("¥" in i for i in ids), "关键标识符提取(含金额)")
|
|
87
|
-
# 截断兜底(无 router)
|
|
88
77
|
msgs = [{"role": "system", "content": "你是助手"}]
|
|
89
78
|
for i in range(10):
|
|
90
79
|
msgs.append({"role": "user", "content": f"长消息{i}" * 20})
|
|
@@ -93,7 +82,6 @@ def test_compaction():
|
|
|
93
82
|
r = c.compact(msgs)
|
|
94
83
|
check(r.method == "truncate" and r.compacted_tokens < r.original_tokens, "无 router 截断兜底有效")
|
|
95
84
|
check(r.messages[-1] == msgs[-1], "最近消息保留")
|
|
96
|
-
# 巨大系统消息 → 放弃无效压缩
|
|
97
85
|
huge = [{"role": "system", "content": "x" * 8000}]
|
|
98
86
|
for i in range(6):
|
|
99
87
|
huge.append({"role": "user", "content": f"m{i}"})
|
|
@@ -108,20 +96,17 @@ def test_hooks():
|
|
|
108
96
|
from foxclaw.skills.hooks_spec import Hooks, normalize_hook, is_valid_hook
|
|
109
97
|
check(normalize_hook("pre_skill") == Hooks.BEFORE_TOOL_CALL, "旧名规整为标准名")
|
|
110
98
|
check(is_valid_hook("on_loop"), "on_loop 是合法钩子")
|
|
111
|
-
# 向后兼容互通
|
|
112
99
|
hm = HookManager()
|
|
113
100
|
fired = []
|
|
114
101
|
hm.register("pre_skill", lambda ctx: fired.append("x") or {})
|
|
115
102
|
hm.fire("before_tool_call", "calc", {})
|
|
116
103
|
check("x" in fired, "旧名注册 + 标准名触发 互通")
|
|
117
|
-
# 阻断权
|
|
118
104
|
hm2 = HookManager()
|
|
119
105
|
hm2.register("before_tool_call", lambda ctx: {"block": True, "reason": "test"})
|
|
120
106
|
check(hm2.fire("before_tool_call", "calc", {}).blocked, "before_tool_call 可阻断")
|
|
121
107
|
hm3 = HookManager()
|
|
122
108
|
hm3.register("after_tool_call", lambda ctx: {"block": True})
|
|
123
109
|
check(not hm3.fire("after_tool_call", "calc", {}).blocked, "after_tool_call 无阻断权")
|
|
124
|
-
# 非法名警告
|
|
125
110
|
hm4 = HookManager()
|
|
126
111
|
hm4.register("typo_hook", lambda ctx: {})
|
|
127
112
|
check(any("未知钩子" in a.get("error", "") for a in hm4.audit_log), "拼错钩子名有警告")
|
|
@@ -135,7 +120,6 @@ def test_idempotency():
|
|
|
135
120
|
check(k1 == k2, "相同输入得相同 key")
|
|
136
121
|
g = IdempotencyGuard(ttl_seconds=3600)
|
|
137
122
|
check(g.check_and_mark(k1) and not g.check_and_mark(k1), "原子查重:首次True重复False")
|
|
138
|
-
# 并发只1个成功
|
|
139
123
|
g2 = IdempotencyGuard(ttl_seconds=3600)
|
|
140
124
|
results, lock = [], threading.Lock()
|
|
141
125
|
def worker():
|
|
@@ -145,11 +129,9 @@ def test_idempotency():
|
|
|
145
129
|
for t in ts: t.start()
|
|
146
130
|
for t in ts: t.join()
|
|
147
131
|
check(sum(results) == 1, "50 线程抢同 key 恰好 1 个成功")
|
|
148
|
-
# TTL
|
|
149
132
|
g3 = IdempotencyGuard(ttl_seconds=0.3)
|
|
150
133
|
g3.check_and_mark("tmp"); time.sleep(0.4)
|
|
151
134
|
check(g3.check_and_mark("tmp"), "TTL 过期后可重新执行")
|
|
152
|
-
# 跨重启
|
|
153
135
|
p = tempfile.mktemp(suffix=".json")
|
|
154
136
|
ga = IdempotencyGuard(persist_path=p); ga.check_and_mark("pk")
|
|
155
137
|
gb = IdempotencyGuard(persist_path=p)
|
|
@@ -161,22 +143,16 @@ def test_recovery():
|
|
|
161
143
|
print("\n【5】重启恢复")
|
|
162
144
|
from foxclaw.core.recovery import RestartRecovery, _pid_alive
|
|
163
145
|
log = tempfile.mktemp(suffix=".jsonl")
|
|
164
|
-
# boot_id 防 pid 复用
|
|
165
146
|
rA = RestartRecovery(log_path=log, boot_id="bootA")
|
|
166
147
|
rA.task_started("t1", "中断任务")
|
|
167
|
-
# 即使 pid 是存活的(复用),boot_id 不同也应识别中断
|
|
168
148
|
rB = RestartRecovery(log_path=log, boot_id="bootB", current_pid=os.getpid())
|
|
169
149
|
interrupted = rB.scan_interrupted()
|
|
170
150
|
check(len(interrupted) == 1 and interrupted[0].task_id == "t1", "boot_id 识别中断(防 pid 复用)")
|
|
171
|
-
# 本次启动自己的任务不误判
|
|
172
151
|
rB.task_started("t2", "本次任务")
|
|
173
152
|
check(not any(t.task_id == "t2" for t in rB.scan_interrupted()), "不误判本次启动的任务")
|
|
174
|
-
# 完成的不算中断
|
|
175
153
|
rB.task_finished("t2")
|
|
176
154
|
check(not any(t.task_id == "t2" for t in rB.scan_interrupted()), "已完成任务不算中断")
|
|
177
155
|
check(_pid_alive(os.getpid()) and not _pid_alive(999999), "pid 存活检测准确")
|
|
178
|
-
# 危险路径回归:探测【外部活进程】必须 True 且【绝不杀死】它
|
|
179
|
-
# (旧实现在 Windows 上 os.kill(pid,0) 会 TerminateProcess 误杀,本用例专抓此坑)
|
|
180
156
|
import subprocess
|
|
181
157
|
proc = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(10)"])
|
|
182
158
|
try:
|
|
@@ -192,13 +168,11 @@ def test_recovery():
|
|
|
192
168
|
def test_steering_queue():
|
|
193
169
|
print("\n【6】干预队列")
|
|
194
170
|
from foxclaw.core.steering_queue import SteeringQueue
|
|
195
|
-
# 基本流
|
|
196
171
|
q = SteeringQueue(lease_seconds=30)
|
|
197
172
|
q.publish("m1", "finance", {"r": "A"})
|
|
198
173
|
leased = q.lease("m1")
|
|
199
174
|
check(len(leased) == 1, "publish→lease")
|
|
200
175
|
check(q.ack(leased[0].item_id, leased[0].lease_id), "ack 成功移除")
|
|
201
|
-
# 并发不重复租用
|
|
202
176
|
q2 = SteeringQueue(lease_seconds=30)
|
|
203
177
|
for i in range(20):
|
|
204
178
|
q2.publish("m2", "a", {"i": i})
|
|
@@ -210,11 +184,9 @@ def test_steering_queue():
|
|
|
210
184
|
for t in ts: t.start()
|
|
211
185
|
for t in ts: t.join()
|
|
212
186
|
check(len(got) == len(set(got)) == 20, "并发租取不重复")
|
|
213
|
-
# 租约过期重入队
|
|
214
187
|
q3 = SteeringQueue(lease_seconds=0.3)
|
|
215
188
|
q3.publish("m3", "a", {}); q3.lease("m3"); time.sleep(0.4)
|
|
216
189
|
check(q3.pending_count() == 1, "租约过期自动重入队(不丢)")
|
|
217
|
-
# drain 尊重租约
|
|
218
190
|
q4 = SteeringQueue(lease_seconds=30)
|
|
219
191
|
q4.publish("m4", "a", {"r": 1}); q4.publish("m4", "a", {"r": 2})
|
|
220
192
|
q4.lease("m4", max_items=1)
|
|
@@ -224,27 +196,21 @@ def test_steering_queue():
|
|
|
224
196
|
def test_token_meter():
|
|
225
197
|
print("\n【7】Token 计量器")
|
|
226
198
|
from foxclaw.core.token_meter import TokenMeter, estimate_tokens
|
|
227
|
-
# 估算非零
|
|
228
199
|
check(estimate_tokens("你好世界hello") > 0, "token 估算非零")
|
|
229
200
|
check(estimate_tokens("") == 0, "空文本 0 token")
|
|
230
201
|
m = TokenMeter()
|
|
231
|
-
# 真实 usage 优先(estimated=False)
|
|
232
202
|
r1 = m.measure_call(system="s", messages=[{"role": "user", "content": "q"}],
|
|
233
203
|
output="a", provider="deepseek", model="deepseek-chat",
|
|
234
204
|
agent_id="finance", usage={"input_tokens": 100, "output_tokens": 200})
|
|
235
205
|
check(not r1.estimated and r1.total_tokens == 300, "真实 usage 优先,total 正确")
|
|
236
|
-
# 无 usage 走估算(estimated=True)
|
|
237
206
|
r2 = m.measure_call(system="你是主管", messages=[{"role": "user", "content": "分析理财预算"}],
|
|
238
207
|
output="已完成分析", provider="deepseek", model="deepseek-chat",
|
|
239
208
|
agent_id="supervisor", usage=None)
|
|
240
209
|
check(r2.estimated and r2.total_tokens > 0, "无 usage 走估算")
|
|
241
|
-
# 按 agent 聚合(agent_id 正确)
|
|
242
210
|
agg = m.by_agent()
|
|
243
211
|
check("finance" in agg and "supervisor" in agg, "按 Agent 聚合(非 unknown)")
|
|
244
212
|
check(agg["finance"]["total_tokens"] == 300, "Agent 聚合数值正确")
|
|
245
|
-
# 报告不崩
|
|
246
213
|
check(len(m.report()) > 0, "文本报告生成")
|
|
247
|
-
# 成本估算
|
|
248
214
|
cost = m.estimate_cost(price_per_1k_input=0.001, price_per_1k_output=0.002)
|
|
249
215
|
check(cost["estimated_cost"] >= 0, "成本估算")
|
|
250
216
|
|
|
@@ -253,11 +219,9 @@ def test_thoughtchain_concurrency():
|
|
|
253
219
|
print("\n【8】ThoughtChain 并发写(共享审计存储)")
|
|
254
220
|
from foxclaw.core.thought_chain import ThoughtChain, ThoughtNode
|
|
255
221
|
tc = ThoughtChain(db_path=tempfile.mktemp(suffix=".db"))
|
|
256
|
-
# WAL 已启用?(并发加固的核心)
|
|
257
222
|
with tc._conn() as c:
|
|
258
223
|
mode = c.execute("PRAGMA journal_mode").fetchone()[0]
|
|
259
224
|
check(mode.lower() == "wal", f"WAL 日志模式已启用(实为 {mode})")
|
|
260
|
-
# N 线程 × M 节点并发写:默认 busy_timeout=0 时这会 database is locked
|
|
261
225
|
N, M, errors = 8, 25, []
|
|
262
226
|
def writer(aid):
|
|
263
227
|
try:
|
|
@@ -288,7 +252,7 @@ def test_harness_action_wiring():
|
|
|
288
252
|
policy_path=tempfile.mktemp(suffix=".yaml"), shadow_rounds=0)
|
|
289
253
|
h.register_goal("分析财务预算", allowed_tool_categories={"read", "finance"},
|
|
290
254
|
baseline_entities=baseline_entities)
|
|
291
|
-
h._shadow_mode = False
|
|
255
|
+
h._shadow_mode = False
|
|
292
256
|
h._block_threshold, h._warn_threshold = 0.25, 0.18
|
|
293
257
|
return h
|
|
294
258
|
|
|
@@ -299,11 +263,9 @@ def test_harness_action_wiring():
|
|
|
299
263
|
tc.add_node(n)
|
|
300
264
|
return n
|
|
301
265
|
|
|
302
|
-
# —— penalize_tool:get_balance 本在 finance 允许集,基线判 0.0 ——
|
|
303
266
|
h1 = fresh({"预算", "收支"})
|
|
304
267
|
check(h1.check(node(tool="get_balance")).scores["tool"] == 0.0,
|
|
305
268
|
"基线:允许的工具 get_balance 工具分 = 0.0")
|
|
306
|
-
# 注入一条 penalize_tool 学习规则(模拟之前 BLOCK 学到的)→ 应立即改变判定
|
|
307
269
|
h1._policy.setdefault("rules", []).append({
|
|
308
270
|
"id": "r1", "source": "harness_learned",
|
|
309
271
|
"action": {"type": "penalize_tool", "tool_pattern": "get_balance"}})
|
|
@@ -312,7 +274,6 @@ def test_harness_action_wiring():
|
|
|
312
274
|
check(r1.scores["tool"] == 1.0 and r1.is_blocked,
|
|
313
275
|
"penalize_tool 生效:同一工具被抬到 1.0 并 BLOCK")
|
|
314
276
|
|
|
315
|
-
# —— restrict_scope:大基线下加 2 个新实体,基线分低(0.2),规则应抬到 1.0 ——
|
|
316
277
|
big = {f"e{i}" for i in range(10)}
|
|
317
278
|
h2 = fresh(big)
|
|
318
279
|
base_scope = h2.check(node(entities=["e0", "x1", "x2"])).scores["scope"]
|
|
@@ -324,16 +285,14 @@ def test_harness_action_wiring():
|
|
|
324
285
|
r2 = h2.check(node(entities=["e0", "x1", "x2"]))
|
|
325
286
|
check(r2.scores["scope"] == 1.0, "restrict_scope 生效:超上限 scope 抬到 1.0")
|
|
326
287
|
|
|
327
|
-
# —— 端到端:一次真实 BLOCK → 【P0-1 新行为】规则进**影子**(不生效),不再立即重建索引 ——
|
|
328
288
|
h3 = fresh({"预算"})
|
|
329
|
-
h3.check(node(tool="delete_everything"))
|
|
289
|
+
h3.check(node(tool="delete_everything"))
|
|
330
290
|
check(not h3._is_penalized_tool("delete_everything"),
|
|
331
291
|
"P0-1:BLOCK 后规则进影子,**不立即生效**(未重建索引)")
|
|
332
292
|
shadow = h3._policy.get("shadow_rules", [])
|
|
333
293
|
check(len(shadow) == 1 and shadow[0]["action"]["type"] == "penalize_tool"
|
|
334
294
|
and shadow[0]["shadow"]["status"] == "observing",
|
|
335
295
|
"P0-1:影子规则已入库且处于 observing")
|
|
336
|
-
# 手工 promote 后应立即生效(升迁路径与自动升迁同构)
|
|
337
296
|
h3._promote_rule(shadow[0])
|
|
338
297
|
check(h3._is_penalized_tool("delete_everything"),
|
|
339
298
|
"P0-1:promote 后规则生效(索引已重建)")
|
|
@@ -350,7 +309,6 @@ def test_harness_thread_safety():
|
|
|
350
309
|
|
|
351
310
|
def worker(k):
|
|
352
311
|
try:
|
|
353
|
-
# 每线程在自己的 context 里注册基线(contextvar 线程独立),再并发 check
|
|
354
312
|
h.register_goal(f"目标{k}", allowed_tool_categories={"read"},
|
|
355
313
|
baseline_entities={"e"})
|
|
356
314
|
for i in range(M):
|
|
@@ -366,7 +324,6 @@ def test_harness_thread_safety():
|
|
|
366
324
|
for t in ts: t.join()
|
|
367
325
|
|
|
368
326
|
check(not errors, f"{N} 线程并发 check 无异常(否则: {errors[:2]})")
|
|
369
|
-
# 锁保护下 _run_count 无丢增量(无锁时 += 竞态会丢)
|
|
370
327
|
check(h._run_count == N * M, f"_run_count 无丢增量({h._run_count}=={N*M})")
|
|
371
328
|
check(len(h._delta_history) == N * M, f"_delta_history 无丢({len(h._delta_history)}=={N*M})")
|
|
372
329
|
|
|
@@ -380,11 +337,10 @@ def test_harness_delta_history_bounded():
|
|
|
380
337
|
h = Harness(agent_id="t2", thought_chain=tc,
|
|
381
338
|
policy_path=tempfile.mktemp(suffix=".yaml"), shadow_rounds=rounds)
|
|
382
339
|
h.register_goal("目标", allowed_tool_categories={"read"}, baseline_entities={"e"})
|
|
383
|
-
for i in range(rounds * 20):
|
|
340
|
+
for i in range(rounds * 20):
|
|
384
341
|
node = ThoughtNode.create(session_id="s", agent_id="t2",
|
|
385
342
|
type="reasoning", content=f"n{i}", conf=0.9)
|
|
386
343
|
tc.add_node(node); h.check(node)
|
|
387
|
-
# 仅 shadow 期(前 rounds 次)append,校准后不再 append → 上限=rounds,不随 run_count 无界增长
|
|
388
344
|
check(len(h._delta_history) <= rounds,
|
|
389
345
|
f"_delta_history 有界(≤{rounds}),实得 {len(h._delta_history)}")
|
|
390
346
|
check(h._run_count == rounds * 20, "run_count 仍完整计数(每次 check 都记)")
|
|
@@ -400,7 +356,7 @@ def test_orchestration_request_log_bounded():
|
|
|
400
356
|
try:
|
|
401
357
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
402
358
|
os_.finance_runtime.set_balance("default", 100000)
|
|
403
|
-
for _ in range(1200):
|
|
359
|
+
for _ in range(1200):
|
|
404
360
|
os_.orchestrator.route_request(CrossAgentRequest(
|
|
405
361
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
406
362
|
payload={"amount": 10, "category": "travel", "account": "default"},
|
|
@@ -419,8 +375,6 @@ def test_hmr_adapter_stats_lock():
|
|
|
419
375
|
hmr = create_memory_backend(storage_path=tempfile.mkdtemp(prefix="hmrlock_"))
|
|
420
376
|
if not hasattr(hmr, "_lock"):
|
|
421
377
|
print(" ~ MockHMR(无 vendor/hmr)无 _lock,跳过"); return
|
|
422
|
-
# 白盒:主线程持锁 → 另一线程的 stats() 必须等锁释放才完成(证明 stats 已进锁)。
|
|
423
|
-
# 补锁前 stats 不持锁 → 会在持锁期间直接完成,order 顺序相反。
|
|
424
378
|
order = []
|
|
425
379
|
hmr._lock.acquire()
|
|
426
380
|
def call_stats():
|
|
@@ -430,7 +384,6 @@ def test_hmr_adapter_stats_lock():
|
|
|
430
384
|
t.join(timeout=5)
|
|
431
385
|
check(order == ["released", "stats_done"],
|
|
432
386
|
f"stats() 应等 _lock 释放才完成(证明补锁生效),实得 {order}")
|
|
433
|
-
# get_system_status 同样补了锁
|
|
434
387
|
hmr._lock.acquire()
|
|
435
388
|
order2 = []
|
|
436
389
|
def call_status():
|
|
@@ -16,7 +16,6 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
18
18
|
|
|
19
|
-
# 隔离项目 .env(本测试不依赖任何真实配置;与批次 2 的隔离策略一致)
|
|
20
19
|
os.environ.setdefault("FOXCLAW_SKIP_DOTENV", "1")
|
|
21
20
|
os.environ.setdefault("HKC_EMBEDDING", "stub")
|
|
22
21
|
|
|
@@ -46,8 +45,6 @@ def _gov_events(spine):
|
|
|
46
45
|
return [e for e in spine.history("governance.", 500)]
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
# ── 1. 审计器单元层 ─────────────────────────────────────────
|
|
50
|
-
|
|
51
48
|
def test_auditor_unit(test_dir):
|
|
52
49
|
print("\n【1】审计器单元:decision_id / 事件 / 落盘 / fail-safe")
|
|
53
50
|
d1, d2 = new_decision_id(), new_decision_id()
|
|
@@ -60,25 +57,22 @@ def test_auditor_unit(test_dir):
|
|
|
60
57
|
detail={"stage": 2}, correlation_id="corr_test_1")
|
|
61
58
|
evs = _gov_events(spine)
|
|
62
59
|
check(len(evs) == 1, f"脊柱有 1 条 governance.decision(实际 {len(evs)})")
|
|
63
|
-
p = evs[0]["payload"]
|
|
60
|
+
p = evs[0]["payload"]
|
|
64
61
|
check(evs[0]["topic"] == TOPIC, "事件主题 governance.decision")
|
|
65
62
|
check(p["decision_id"] == did and p["layer"] == "permission"
|
|
66
63
|
and p["verdict"] == "deny" and p["reason"] == "命中 deny 规则",
|
|
67
64
|
"事件 payload 字段完整")
|
|
68
65
|
check(evs[0]["correlation_id"] == "corr_test_1", "correlation_id 透传到事件")
|
|
69
66
|
|
|
70
|
-
# 显式 decision_id 被尊重(P3-4:PolicyGate 生成后交给审计器,不能再换一个)
|
|
71
67
|
did2 = aud.record(layer="skill", actor="", action="invoke:x", verdict="success",
|
|
72
68
|
decision_id="dec_explicit01")
|
|
73
69
|
check(did2 == "dec_explicit01", "显式 decision_id 原样返回")
|
|
74
70
|
|
|
75
|
-
# 落盘与事件一致
|
|
76
71
|
recs = aud.read()
|
|
77
72
|
check(len(recs) == 2, f"落盘 2 条(实际 {len(recs)})")
|
|
78
73
|
check(recs[0]["decision_id"] == "dec_explicit01", "read() 新→旧排序")
|
|
79
74
|
check(recs[1]["correlation_id"] == "corr_test_1", "落盘记录含 correlation_id")
|
|
80
75
|
|
|
81
|
-
# fail-safe:spine 爆炸不影响 record
|
|
82
76
|
class BoomSpine:
|
|
83
77
|
def publish(self, *a, **k):
|
|
84
78
|
raise RuntimeError("spine boom")
|
|
@@ -87,7 +81,6 @@ def test_auditor_unit(test_dir):
|
|
|
87
81
|
verdict="warn", reason="语义漂移")
|
|
88
82
|
check(did3.startswith("dec_"), "spine 异常时 record 仍返回 decision_id(fail-safe)")
|
|
89
83
|
|
|
90
|
-
# 开关
|
|
91
84
|
os.environ[ENV_SWITCH] = "0"
|
|
92
85
|
try:
|
|
93
86
|
check(attach(spine, test_dir) is None, f"{ENV_SWITCH}=0 时 attach 返回 None")
|
|
@@ -96,8 +89,6 @@ def test_auditor_unit(test_dir):
|
|
|
96
89
|
check(attach(spine, test_dir) is not None, "默认(未设开关)attach 正常返回")
|
|
97
90
|
|
|
98
91
|
|
|
99
|
-
# ── 2. PolicyGate(P1-2/P3-4)─────────────────────────────────
|
|
100
|
-
|
|
101
92
|
def _make_gate(test_dir, spine=None):
|
|
102
93
|
from foxclaw.kernel.policy_gate import PolicyGate
|
|
103
94
|
from foxclaw.skills.permission import evaluate
|
|
@@ -117,12 +108,10 @@ def test_policy_gate(test_dir):
|
|
|
117
108
|
print("\n【2】PolicyGate:decision_id(P3-4)+ 非 allow 审计(P1-2)")
|
|
118
109
|
gate, aud, spine = _make_gate(test_dir)
|
|
119
110
|
|
|
120
|
-
# allow:有 decision_id,但**不**记审计(高频常态不淹没审计流)
|
|
121
111
|
v_allow = gate.check_action(_make_skill(), {})
|
|
122
112
|
check(v_allow.decision_id.startswith("dec_"), "allow 裁决也带 decision_id(供执行审计 join)")
|
|
123
113
|
check(len(_gov_events(spine)) == 0, "allow 不记 governance 事件")
|
|
124
114
|
|
|
125
|
-
# deny:记审计,layer=permission,decision_id 一致
|
|
126
115
|
from foxclaw.skills.permission import PermissionRules
|
|
127
116
|
rules = PermissionRules(deny=["demo.*"])
|
|
128
117
|
v_deny = gate.check_action(_make_skill(), {}, rules=rules)
|
|
@@ -135,13 +124,11 @@ def test_policy_gate(test_dir):
|
|
|
135
124
|
check(p["decision_id"] == v_deny.decision_id, "审计 decision_id 与 Verdict 一致(P3-4)")
|
|
136
125
|
check(p["detail"]["stage"] == 2, "detail 含四阶段信息")
|
|
137
126
|
|
|
138
|
-
# ask → confirm
|
|
139
127
|
v_ask = gate.check_action(_make_skill("x.pay", side_effect="pay"), {})
|
|
140
128
|
check(v_ask.level == "confirm", "ask 归一为 confirm(行为不变)")
|
|
141
129
|
evs = _gov_events(spine)
|
|
142
130
|
check(len(evs) == 2 and evs[1]["payload"]["verdict"] == "confirm", "confirm 也记审计")
|
|
143
131
|
|
|
144
|
-
# 推理准入:BLOCK 记,PASS 不记
|
|
145
132
|
from foxclaw.harness.harness import HarnessResult
|
|
146
133
|
class FakeHarness:
|
|
147
134
|
def __init__(self, status): self._s = status
|
|
@@ -159,14 +146,11 @@ def test_policy_gate(test_dir):
|
|
|
159
146
|
gate.check_reasoning(FakeHarness("PASS"), FakeNode())
|
|
160
147
|
check(len(_gov_events(spine)) == 3, "PASS 不记审计")
|
|
161
148
|
|
|
162
|
-
# 向后兼容:旧式位置参数构造(无 decision_id)仍可用
|
|
163
149
|
from foxclaw.kernel.policy_gate import Verdict
|
|
164
150
|
v_old = Verdict(True, "allow", "无动作评估器(降级放行)", "permission", None)
|
|
165
151
|
check(v_old.decision_id == "" and v_old.allowed, "Verdict 位置参数构造向后兼容")
|
|
166
152
|
|
|
167
153
|
|
|
168
|
-
# ── 3. Orchestrator(P1-3)───────────────────────────────────
|
|
169
|
-
|
|
170
154
|
class _FakeCaps:
|
|
171
155
|
def __init__(self, agents): self._agents = agents
|
|
172
156
|
def get(self, name): return self._agents if name == "agents" else None
|
|
@@ -185,7 +169,6 @@ def test_orchestrator(test_dir):
|
|
|
185
169
|
from foxclaw.orchestration import Orchestrator, CrossAgentRequest
|
|
186
170
|
aud, spine = _fresh_auditor(test_dir)
|
|
187
171
|
|
|
188
|
-
# 三种终态 + 目标不存在(P1-0 路径)
|
|
189
172
|
agents = {"finance": _FakeAgent([("approved", "预算充足"),
|
|
190
173
|
("rejected", "超出月度预算")])}
|
|
191
174
|
orch = Orchestrator(_FakeCaps(agents), auditor=aud)
|
|
@@ -211,33 +194,29 @@ def test_orchestrator(test_dir):
|
|
|
211
194
|
and evs[2]["payload"]["correlation_id"] == r3.id,
|
|
212
195
|
"correlation_id == req.id(协商链 join 键)")
|
|
213
196
|
|
|
214
|
-
# 协商链:拒绝 → 让步 → 批准,两轮裁决同一 correlation_id
|
|
215
197
|
agents2 = {"finance": _FakeAgent([("rejected", "太贵"),
|
|
216
198
|
("approved", "这个价可以")])}
|
|
217
199
|
requester = _FakeAgent([])
|
|
218
|
-
requester.concede = lambda req, verdict: {"amount": 50}
|
|
200
|
+
requester.concede = lambda req, verdict: {"amount": 50}
|
|
219
201
|
orch2 = Orchestrator(_FakeCaps({"finance": agents2["finance"],
|
|
220
202
|
"admin": requester}), auditor=aud)
|
|
221
203
|
req = CrossAgentRequest(from_agent="admin", to_agent="finance",
|
|
222
204
|
request_type="budget_request", payload={"amount": 500})
|
|
223
205
|
final = orch2.negotiate(req)
|
|
224
206
|
check(final.status == "approved", "协商后获批(行为不变)")
|
|
225
|
-
evs2 = _gov_events(spine)[3:]
|
|
207
|
+
evs2 = _gov_events(spine)[3:]
|
|
226
208
|
check(len(evs2) == 2, f"协商两轮裁决各记一条(实际 {len(evs2)})")
|
|
227
209
|
check(all(e["payload"]["correlation_id"] == req.id for e in evs2),
|
|
228
210
|
"多轮裁决共用 req.id——协商链可 join(P3-4 跨层关联)")
|
|
229
211
|
check(evs2[0]["payload"]["verdict"] == "deny" and evs2[1]["payload"]["verdict"] == "allow",
|
|
230
212
|
"协商链 deny→allow 轨迹完整")
|
|
231
213
|
|
|
232
|
-
# 无 auditor:standalone 不崩、不记
|
|
233
214
|
orch3 = Orchestrator(_FakeCaps({}), auditor=None)
|
|
234
215
|
orch3.route_request(CrossAgentRequest(from_agent="a", to_agent="b",
|
|
235
216
|
request_type="info_request", payload={}))
|
|
236
217
|
check(len(_gov_events(spine)) == 5, "auditor=None 时不记审计(standalone 兼容)")
|
|
237
218
|
|
|
238
219
|
|
|
239
|
-
# ── 4. SkillAdapter(P2-0/P3-4 核心:裁决→执行 join)──────────
|
|
240
|
-
|
|
241
220
|
def test_skill_adapter(test_dir):
|
|
242
221
|
print("\n【4】SkillAdapter:执行终态审计 + 裁决↔执行 decision_id join(P2-0/P3-4)")
|
|
243
222
|
from foxclaw.skills import SkillRegistry, SkillAdapter, SkillSpec
|
|
@@ -246,7 +225,6 @@ def test_skill_adapter(test_dir):
|
|
|
246
225
|
reg = SkillRegistry()
|
|
247
226
|
adapter = SkillAdapter(reg, policy_gate=gate)
|
|
248
227
|
|
|
249
|
-
# ① 只读 skill 成功执行 → skill success 审计,correlation_id == 权限裁决 decision_id
|
|
250
228
|
ok_skill = SkillSpec(id="demo.read", name="读", description="只读",
|
|
251
229
|
side_effect="read", handler=lambda **k: {"value": 42})
|
|
252
230
|
res = adapter.invoke(ok_skill, {})
|
|
@@ -258,19 +236,15 @@ def test_skill_adapter(test_dir):
|
|
|
258
236
|
corr = p["correlation_id"]
|
|
259
237
|
check(corr.startswith("dec_"), f"执行审计带裁决 decision_id 作 correlation_id: {corr}")
|
|
260
238
|
|
|
261
|
-
# ② 该 correlation_id 能 join 到真实裁决:再触发一次**非 allow** 裁决,
|
|
262
|
-
# 验证 permission 记录的 decision_id 与后续执行审计的 correlation_id 同链
|
|
263
239
|
pay_skill = SkillSpec(id="demo.pay", name="付", description="支付",
|
|
264
240
|
side_effect="pay", handler=lambda **k: {"done": True})
|
|
265
|
-
res2 = adapter.invoke(pay_skill, {}, confirmed=True)
|
|
241
|
+
res2 = adapter.invoke(pay_skill, {}, confirmed=True)
|
|
266
242
|
check(res2.success, "已确认的支付 skill 执行成功")
|
|
267
|
-
# 手工复演同参裁决,验证 decision_id 链:invoke 内部那次裁决的 id == 执行审计 correlation_id
|
|
268
243
|
exec_recs = [e["payload"] for e in _gov_events(spine) if e["payload"]["layer"] == "skill"]
|
|
269
244
|
check(len(exec_recs) == 2, "两次成功执行各记一条")
|
|
270
245
|
check(all(r["correlation_id"].startswith("dec_") for r in exec_recs),
|
|
271
246
|
"每条执行审计都挂在某个裁决 decision_id 上")
|
|
272
247
|
|
|
273
|
-
# ③ deny:permission 记裁决,skill 层**不重复**记执行
|
|
274
248
|
adapter_deny = SkillAdapter(reg, policy_gate=gate,
|
|
275
249
|
rules=PermissionRules(deny=["evil.*"]))
|
|
276
250
|
evil = SkillSpec(id="evil.wipe", name="清", description="危险",
|
|
@@ -283,7 +257,6 @@ def test_skill_adapter(test_dir):
|
|
|
283
257
|
check(len(perm_recs) == 1 and perm_recs[0]["verdict"] == "deny", "deny 裁决被记录")
|
|
284
258
|
check(len(skill_recs_after) == 2, "deny 后 skill 层不重复记录(仍只有前两条执行审计)")
|
|
285
259
|
|
|
286
|
-
# ④ 断路器出口:权限过了但断路器开 → skill deny 审计,correlation_id 挂裁决
|
|
287
260
|
from foxclaw.skills.lifecycle import CircuitBreaker
|
|
288
261
|
br = CircuitBreaker()
|
|
289
262
|
for _ in range(10):
|
|
@@ -299,14 +272,8 @@ def test_skill_adapter(test_dir):
|
|
|
299
272
|
check(last["correlation_id"].startswith("dec_"), "断路器审计也挂在裁决 decision_id 上")
|
|
300
273
|
|
|
301
274
|
|
|
302
|
-
# ── 5. WS 可见性 + 真实装配端到端 ───────────────────────────
|
|
303
|
-
|
|
304
275
|
def test_ws_visibility_and_wiring(test_dir):
|
|
305
276
|
print("\n【5】WS 可见性 + 真实装配端到端")
|
|
306
|
-
# governance.decision 不能被 WS 静音逻辑吞掉(验收:governance.* 事件 WS 可见)。
|
|
307
|
-
# _WS_MUTED 是 api 投影闭包的局部变量,无法 import —— 改为源码级断言:静音规则只有
|
|
308
|
-
# 「agent.cognition.* 前缀」与「_WS_MUTED 元组」两条,二者都不得命中 governance.*。
|
|
309
|
-
# 这同时是一道防退化闸:将来若有人把 governance.* 加进静音列表,本测试会红。
|
|
310
277
|
import inspect, re as _re
|
|
311
278
|
import foxclaw.api as _api_mod
|
|
312
279
|
src = inspect.getsource(_api_mod)
|
|
@@ -316,7 +283,6 @@ def test_ws_visibility_and_wiring(test_dir):
|
|
|
316
283
|
check(not "governance.decision".startswith("agent.cognition."),
|
|
317
284
|
"governance.decision 不命中 agent.cognition.* 静音前缀")
|
|
318
285
|
|
|
319
|
-
# 真实装配:三处同一 auditor;一次真共享写入之外的治理动作落盘
|
|
320
286
|
from foxclaw import Foxclaw
|
|
321
287
|
d = tempfile.mkdtemp(prefix="foxclaw_gov_e2e_", dir=test_dir)
|
|
322
288
|
o = Foxclaw(data_dir=d, shadow_rounds=2)
|
|
@@ -324,7 +290,6 @@ def test_ws_visibility_and_wiring(test_dir):
|
|
|
324
290
|
check(o.policy_gate.auditor is o.governance_auditor, "PolicyGate 持同一 auditor")
|
|
325
291
|
check(o.orchestrator._auditor is o.governance_auditor, "Orchestrator 持同一 auditor")
|
|
326
292
|
|
|
327
|
-
# 端到端裁决:目标不存在的跨 Agent 请求 → 审计事件 + 落盘
|
|
328
293
|
from foxclaw.orchestration import CrossAgentRequest
|
|
329
294
|
o.orchestrator.route_request(CrossAgentRequest(
|
|
330
295
|
from_agent="admin", to_agent="ghost", request_type="info_request", payload={}))
|
|
@@ -35,13 +35,10 @@ def _sess():
|
|
|
35
35
|
def test_standalone_block():
|
|
36
36
|
print("\n【1】独立可用 + 工具越权拦截")
|
|
37
37
|
gov = _sess()
|
|
38
|
-
# 正常只读:不抛(可能 PASS/WARN,但绝不 BLOCK)
|
|
39
38
|
r_ok = gov.observe("call get_balance to read balance", tool="get_balance")
|
|
40
39
|
check(not r_ok.blocked, "只读工具 get_balance 不被拦")
|
|
41
|
-
# 越权:delete 不在 read 允许集 → 硬拦
|
|
42
40
|
r_bad = gov.observe("call delete_account", tool="delete_account")
|
|
43
41
|
check(r_bad.blocked and r_bad.dimension == "tool", "delete_account 被 BLOCK(维度=tool)")
|
|
44
|
-
# guard 风格:越权抛异常
|
|
45
42
|
raised = False
|
|
46
43
|
try:
|
|
47
44
|
gov.guard("call drop_table", tool="drop_table")
|
|
@@ -56,7 +53,7 @@ def test_decorator_sync_async():
|
|
|
56
53
|
|
|
57
54
|
@governed(gov)
|
|
58
55
|
def delete_row(x):
|
|
59
|
-
return f"deleted {x}"
|
|
56
|
+
return f"deleted {x}"
|
|
60
57
|
|
|
61
58
|
executed = {"sync": False}
|
|
62
59
|
try:
|
|
@@ -78,7 +75,6 @@ def test_decorator_sync_async():
|
|
|
78
75
|
return False
|
|
79
76
|
check(not asyncio.run(run()), "异步:越权协程被拦,未执行")
|
|
80
77
|
|
|
81
|
-
# 正常只读函数正常执行
|
|
82
78
|
@governed(gov, tool_name="get_balance")
|
|
83
79
|
def read_balance():
|
|
84
80
|
return 100
|
|
@@ -96,7 +92,6 @@ def test_audit_and_loop():
|
|
|
96
92
|
for n in audit), "越权步骤在审计链里标为 BLOCK")
|
|
97
93
|
check(gov.summary()["blocked_this_session"] == 1, "摘要记录本会话拦截数")
|
|
98
94
|
|
|
99
|
-
# 循环检测:反复同一工具 → loop 告警
|
|
100
95
|
gov2 = _sess()
|
|
101
96
|
loop_hit = None
|
|
102
97
|
for _ in range(9):
|
|
@@ -108,7 +103,6 @@ def test_audit_and_loop():
|
|
|
108
103
|
|
|
109
104
|
def test_import_decoupling():
|
|
110
105
|
print("\n【4】物理解耦:导入治理中间件不连带拉入 Agent 框架")
|
|
111
|
-
# 必须在**全新子进程**里验(本进程里其它测试早已 import 了 agents,sys.modules 被污染)
|
|
112
106
|
code = (
|
|
113
107
|
"import sys, foxclaw.governance;"
|
|
114
108
|
"bad=[m for m in ('foxclaw.agents.agents','foxclaw.skills',"
|