foxclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -52,11 +52,9 @@ def test_process_manager():
|
|
|
52
52
|
check(pm.get(p.process_id).state == "completed", "完整生命周期到 completed")
|
|
53
53
|
check(pm.get(p.process_id).state_history == ["created", "queued", "scheduled", "running", "completed"],
|
|
54
54
|
"状态历史完整")
|
|
55
|
-
# 幂等:已终态再标记不崩溃,且**不重复扣减**活跃计数
|
|
56
55
|
r = pm.mark_failed(p.process_id, "二次")
|
|
57
56
|
check(r is not None and r.state == "completed", "终态重复标记幂等不崩溃")
|
|
58
57
|
check(pm.active_count() == 0, "终态重复标记不重复扣减活跃计数(仍为0,不为负)")
|
|
59
|
-
# 非法流转拦截
|
|
60
58
|
p2 = pm.create("x", schedule={"priority": 0.1})
|
|
61
59
|
p2.state = "created"
|
|
62
60
|
try:
|
|
@@ -89,7 +87,6 @@ def test_truth_pipeline():
|
|
|
89
87
|
bus = SystemBus()
|
|
90
88
|
hmr = FakeHMR()
|
|
91
89
|
tp = TruthPipeline(bus, hmr=hmr)
|
|
92
|
-
# 【P3-9】TruthPipeline.emit_execution 已删除:事件名即契约,直接经 bus 入队。
|
|
93
90
|
bus.publish("execution.completed", {"agent_id": "finance", "task": "分析预算",
|
|
94
91
|
"result": "预算正常", "blocked": False})
|
|
95
92
|
check(tp.last_trace is not None, "执行→审计 trace 生成")
|
|
@@ -110,11 +107,9 @@ def test_cognitive_state():
|
|
|
110
107
|
s = t.snapshot()
|
|
111
108
|
check(s["active_goal"] == "分析财务", "活跃目标聚合")
|
|
112
109
|
check(s["semantic_focus"] == "reasoning", "语义焦点聚合")
|
|
113
|
-
# 去重
|
|
114
110
|
for _ in range(5):
|
|
115
111
|
bus.publish("memory.updated", {"memory_update": {"content": "同样"}})
|
|
116
112
|
check(len(t.snapshot()["activated_memories"]) == 1, "重复记忆去重")
|
|
117
|
-
# None 防护
|
|
118
113
|
class M: payload = None
|
|
119
114
|
t._on_exec(M())
|
|
120
115
|
check(True, "None payload 不崩溃")
|
|
@@ -147,8 +142,7 @@ def test_trust_single_source():
|
|
|
147
142
|
from foxclaw.skills import permission as perm
|
|
148
143
|
from foxclaw.skills.permission import evaluate
|
|
149
144
|
check(not hasattr(perm, "TRUST_LEVEL"), "permission.TRUST_LEVEL 死字典已删(不再双真源)")
|
|
150
|
-
|
|
151
|
-
expect = {"local": False, "web_api": False, "mcp": True} # behavior-preserving 基线
|
|
145
|
+
expect = {"local": False, "web_api": False, "mcp": True}
|
|
152
146
|
for kind, want_dg in expect.items():
|
|
153
147
|
for source in ("builtin", "user"):
|
|
154
148
|
s = SkillSpec(id=f"s_{kind}_{source}", name="t", description="d",
|
|
@@ -174,7 +168,6 @@ def test_breaker_hooks():
|
|
|
174
168
|
check(ad.breaker.status("flaky")["state"] == "open", "连续失败3次断路器开闸")
|
|
175
169
|
r = ad.invoke(reg.get("flaky"), {})
|
|
176
170
|
check(r.status == "circuit_open", "开闸后停止调用")
|
|
177
|
-
# 钩子
|
|
178
171
|
log = []
|
|
179
172
|
hm = HookManager()
|
|
180
173
|
hm.register("pre_skill", lambda ctx: log.append("pre"))
|
|
@@ -193,13 +186,10 @@ def test_ahs_pipeline():
|
|
|
193
186
|
reg = build_default_registry()
|
|
194
187
|
ad = SkillAdapter(reg)
|
|
195
188
|
pipe = AHSPipeline(reg, ad)
|
|
196
|
-
# router 否定词
|
|
197
189
|
r = route("不要计算", reg)
|
|
198
190
|
check(r.primary != "calculator", "否定词检测:不要计算→不选calculator")
|
|
199
|
-
# planner 拆解
|
|
200
191
|
p = plan("先算 5+5 然后再算 9*9", reg)
|
|
201
192
|
check(len(p.tasks) == 2, "planner 多步拆解")
|
|
202
|
-
# 端到端
|
|
203
193
|
res = pipe.run("帮我算一下 12*12")
|
|
204
194
|
check(res.success, "AHS 执行链端到端成功")
|
|
205
195
|
|
|
@@ -27,23 +27,19 @@ def main():
|
|
|
27
27
|
print(f"\n采集 {n} 个知识点")
|
|
28
28
|
check(n == 8, f"采集 8 个知识点(实为 {n})")
|
|
29
29
|
|
|
30
|
-
# 1. 间隔重复:模拟几次复习
|
|
31
30
|
print("\n【固化:间隔重复】")
|
|
32
31
|
ids = list(rt.items.keys())
|
|
33
32
|
check(len(ids) >= 4, f"至少 4 个知识点可供复习(实为 {len(ids)})")
|
|
34
|
-
|
|
35
|
-
rt.review(ids[0], grade=5) # 完美
|
|
33
|
+
rt.review(ids[0], grade=5)
|
|
36
34
|
rt.review(ids[0], grade=4)
|
|
37
35
|
rt.review(ids[1], grade=4)
|
|
38
|
-
rt.review(ids[2], grade=1)
|
|
39
|
-
rt.review(ids[2], grade=2)
|
|
36
|
+
rt.review(ids[2], grade=1)
|
|
37
|
+
rt.review(ids[2], grade=2)
|
|
40
38
|
for iid in ids[:4]:
|
|
41
39
|
st = rt.review_states[iid]
|
|
42
40
|
item = rt.items[iid]
|
|
43
41
|
print(f" 掌握度 {st.mastery:.2f} | 间隔 {st.interval}天 | "
|
|
44
42
|
f"复习{st.total_reviews}次 遗忘{st.lapses}次 | {item.content[:24]}...")
|
|
45
|
-
# SM-2 的语义后果:连续答不好应累积 lapses,且掌握度被压低。
|
|
46
|
-
# 这是"间隔重复真的在起作用"的核心断言 —— 只打印 0.60/0.11 两个数字不算验证。
|
|
47
43
|
st_good, st_bad = rt.review_states[ids[0]], rt.review_states[ids[2]]
|
|
48
44
|
check(st_bad.lapses == 2, f"连错两次的知识点累积 lapses=2(实为 {st_bad.lapses})")
|
|
49
45
|
check(st_bad.mastery < st_good.mastery,
|
|
@@ -51,7 +47,6 @@ def main():
|
|
|
51
47
|
check(st_good.total_reviews == 2 and st_bad.total_reviews == 2,
|
|
52
48
|
"复习次数被正确累计(各 2 次)")
|
|
53
49
|
|
|
54
|
-
# 2. 知识图谱:构建连接
|
|
55
50
|
print("\n【连接:知识图谱】")
|
|
56
51
|
conn = rt.build_connections(semantic_threshold=0.5)
|
|
57
52
|
print(f" 同主题边: {conn['topic_edges']} · 语义边: {conn['semantic_edges']}")
|
|
@@ -61,12 +56,10 @@ def main():
|
|
|
61
56
|
print(f" 边类型: {g['edge_types']}")
|
|
62
57
|
check(conn["topic_edges"] > 0, f"同主题连接已建立({conn['topic_edges']} 条)")
|
|
63
58
|
check(g["items"] == n, f"图谱节点数 = 知识点数({g['items']} == {n})")
|
|
64
|
-
# 结构不变式:总边数必须等于各类边之和,否则说明有一类边被漏计/重复计
|
|
65
59
|
check(g["edges"] == conn["topic_edges"] + conn["semantic_edges"],
|
|
66
60
|
f"总边数 = 同主题边 + 语义边({g['edges']} == {conn['topic_edges']}+{conn['semantic_edges']})")
|
|
67
61
|
check(g["clusters"] > 0, f"聚类非空({g['clusters']} 个知识簇)")
|
|
68
62
|
|
|
69
|
-
# 3. 连接发现:某知识点的关联
|
|
70
63
|
print("\n【连接发现】")
|
|
71
64
|
related = rt.related(ids[0])
|
|
72
65
|
base = rt.items[ids[0]].content[:20]
|
|
@@ -77,7 +70,6 @@ def main():
|
|
|
77
70
|
check(all(("edge_type" in x and "weight" in x and "item" in x) for x in related),
|
|
78
71
|
"每条关联都带 edge_type / weight / item 结构")
|
|
79
72
|
|
|
80
|
-
# 4. 综合分析
|
|
81
73
|
print("\n【综合分析】")
|
|
82
74
|
rep = rt.analyze()
|
|
83
75
|
print(f" 知识点 {rep['total_items']} · 平均掌握度 {rep['avg_mastery']}")
|
|
@@ -89,7 +81,6 @@ def main():
|
|
|
89
81
|
check(rep["weakest_topic"] in rep["topic_mastery"],
|
|
90
82
|
f"最薄弱主题来自主题掌握度表(weakest_topic={rep['weakest_topic']})")
|
|
91
83
|
|
|
92
|
-
# 5. 学习策略裁决
|
|
93
84
|
print("\n【学习策略裁决】")
|
|
94
85
|
for v in rep["strategy"]:
|
|
95
86
|
print(f" [{v['action']}] {v['message']}")
|
|
@@ -11,7 +11,6 @@ import os, sys, json, tempfile
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
13
|
|
|
14
|
-
# ── 环境隔离:必须在 import foxclaw 之前 ──────────────────────────────
|
|
15
14
|
for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "GOOGLE_API_KEY",
|
|
16
15
|
"DASHSCOPE_API_KEY", "MINIMAX_API_KEY", "LLM_API_KEY", "FOXCLAW_API_KEY"):
|
|
17
16
|
os.environ.pop(_k, None)
|
|
@@ -32,17 +31,14 @@ import foxclaw.api as _api
|
|
|
32
31
|
from foxclaw.api.routes import llm_admin
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
# ── 1. BASE URL 规范化 ───────────────────────────────────────────────
|
|
36
|
-
|
|
37
34
|
@pytest.mark.parametrize("raw,expected", [
|
|
38
35
|
("https://api.deepseek.com", "https://api.deepseek.com/v1"),
|
|
39
36
|
("https://api.deepseek.com/", "https://api.deepseek.com/v1"),
|
|
40
37
|
("https://api.deepseek.com/v1", "https://api.deepseek.com/v1"),
|
|
41
38
|
("https://api.deepseek.com/v1/", "https://api.deepseek.com/v1"),
|
|
42
39
|
(" https://api.openai.com/v1 ", "https://api.openai.com/v1"),
|
|
43
|
-
("api.deepseek.com", "https://api.deepseek.com/v1"),
|
|
40
|
+
("api.deepseek.com", "https://api.deepseek.com/v1"),
|
|
44
41
|
("api.deepseek.com/v1", "https://api.deepseek.com/v1"),
|
|
45
|
-
# 已有非 /v1 的 path 必须原样保留(通义兼容模式 / 自建网关 /openai/v1)
|
|
46
42
|
("https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
47
43
|
"https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
|
48
44
|
("http://localhost:11434/v1", "http://localhost:11434/v1"),
|
|
@@ -50,7 +46,6 @@ from foxclaw.api.routes import llm_admin
|
|
|
50
46
|
])
|
|
51
47
|
def test_normalize_base_url(raw, expected):
|
|
52
48
|
assert normalize_base_url(raw) == expected
|
|
53
|
-
# 幂等:规范化两次结果不变(前端 blur + 后端保存 + router 构造都会过一遍)
|
|
54
49
|
assert normalize_base_url(expected) == expected
|
|
55
50
|
|
|
56
51
|
|
|
@@ -60,8 +55,6 @@ def test_normalize_base_url_rejects(bad):
|
|
|
60
55
|
normalize_base_url(bad)
|
|
61
56
|
|
|
62
57
|
|
|
63
|
-
# ── 2. custom 必须被当作 openai-compatible 家族成员 ──────────────────
|
|
64
|
-
|
|
65
58
|
def test_custom_is_openai_family():
|
|
66
59
|
assert "custom" in ModelRouter._OPENAI_FAMILY, \
|
|
67
60
|
"custom 漏了会让流式退化为一次性输出、工具能力静默关闭"
|
|
@@ -71,28 +64,22 @@ def test_custom_is_openai_family():
|
|
|
71
64
|
def test_custom_provider_supports_tools():
|
|
72
65
|
r = ModelRouter(provider="stub")
|
|
73
66
|
r.provider = "custom"
|
|
74
|
-
r._client = object()
|
|
67
|
+
r._client = object()
|
|
75
68
|
assert r.supports_tools() is True
|
|
76
69
|
|
|
77
70
|
|
|
78
|
-
# ── 3. stream() 必须 reload 选择(聊天主路径)─────────────────────────
|
|
79
|
-
|
|
80
71
|
def test_stream_reloads_selection():
|
|
81
72
|
r = ModelRouter(provider="stub")
|
|
82
73
|
hits = []
|
|
83
|
-
r._maybe_reload_selection = lambda: hits.append(True)
|
|
74
|
+
r._maybe_reload_selection = lambda: hits.append(True)
|
|
84
75
|
list(r.stream("sys", [{"role": "user", "content": "hi"}]))
|
|
85
76
|
assert hits, "stream() 未调用 _maybe_reload_selection → 切模型后流式对话不生效"
|
|
86
77
|
|
|
87
78
|
|
|
88
|
-
# ── 4. 保存 / 切换端点 ───────────────────────────────────────────────
|
|
89
|
-
|
|
90
79
|
@pytest.fixture()
|
|
91
80
|
def fresh_store(monkeypatch):
|
|
92
81
|
"""每个用例一份干净的 custom_models.json + .current_model。"""
|
|
93
82
|
d = Path(tempfile.mkdtemp(prefix="llm_admin_store_"))
|
|
94
|
-
# router 侧按 env 现算路径(_custom_models_path / _selection_path),必须一起指过来,
|
|
95
|
-
# 否则端点写 A 目录、router 读 B 目录 → 预检假失败。
|
|
96
83
|
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(d))
|
|
97
84
|
monkeypatch.setattr(llm_admin, "CUSTOM_MODELS_PATH", d / "custom_models.json")
|
|
98
85
|
monkeypatch.setattr(llm_admin, "CURRENT_MODEL_PATH", d / ".current_model")
|
|
@@ -110,7 +97,6 @@ def test_add_normalizes_base_url(fresh_store):
|
|
|
110
97
|
saved = json.loads((fresh_store / "custom_models.json").read_text(encoding="utf-8"))
|
|
111
98
|
assert saved[0]["base_url"] == "https://api.deepseek.com/v1"
|
|
112
99
|
|
|
113
|
-
# 更新时同样规范化
|
|
114
100
|
import asyncio
|
|
115
101
|
asyncio.run(llm_admin.update_custom_model("deepseek", llm_admin._CustomUpdateReq(
|
|
116
102
|
base_url="https://api.deepseek.com")))
|
|
@@ -133,7 +119,7 @@ def test_select_custom_takes_effect_immediately(fresh_store, monkeypatch):
|
|
|
133
119
|
pass
|
|
134
120
|
fake = _FakeOS()
|
|
135
121
|
fake.router = router
|
|
136
|
-
monkeypatch.setattr(_api, "_os_instance", fake, raising=False)
|
|
122
|
+
monkeypatch.setattr(_api._state, "_os_instance", fake, raising=False)
|
|
137
123
|
|
|
138
124
|
import asyncio
|
|
139
125
|
out = asyncio.run(llm_admin.select_model(
|
|
@@ -143,7 +129,6 @@ def test_select_custom_takes_effect_immediately(fresh_store, monkeypatch):
|
|
|
143
129
|
assert out["runtime"]["provider"] == "custom"
|
|
144
130
|
assert out["runtime"]["model"] == "deepseek-chat"
|
|
145
131
|
assert router.provider == "custom" and router.model == "deepseek-chat"
|
|
146
|
-
# .current_model 落盘,重启后 lazy reload 仍能读到
|
|
147
132
|
cur = json.loads((fresh_store / ".current_model").read_text(encoding="utf-8"))
|
|
148
133
|
assert cur["source"] == "custom" and cur["name"] == "deepseek"
|
|
149
134
|
|
|
@@ -83,20 +83,15 @@ def test_build_plan_tool_schema():
|
|
|
83
83
|
|
|
84
84
|
def test_llm_plan_failsafe():
|
|
85
85
|
print("\n【3】fail-safe:不支持/不产出/解析失败/空输入/抛异常 → None")
|
|
86
|
-
# 不支持 tools
|
|
87
86
|
r = _FakeRouter(result={"supported": False})
|
|
88
87
|
assert llm_plan(r, "任务", _catalog()) is None
|
|
89
|
-
# 模型没调用工具(空 tool_calls)
|
|
90
88
|
r2 = _FakeRouter(result={"supported": True, "tool_calls": []})
|
|
91
89
|
assert llm_plan(r2, "任务", _catalog()) is None
|
|
92
|
-
# args 解析失败
|
|
93
90
|
r3 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
94
91
|
{"id": "c1", "name": "submit_plan", "args": {}, "args_parse_failed": True}]})
|
|
95
92
|
assert llm_plan(r3, "任务", _catalog()) is None
|
|
96
|
-
# 空 catalog / 空 task
|
|
97
93
|
assert llm_plan(r2, "任务", {}) is None
|
|
98
94
|
assert llm_plan(r2, "", _catalog()) is None
|
|
99
|
-
# call_tools 抛异常 → None(不冒泡)
|
|
100
95
|
|
|
101
96
|
class _Boom:
|
|
102
97
|
def call_tools(self, *a, **k):
|
|
@@ -120,13 +115,11 @@ def test_llm_plan_parse():
|
|
|
120
115
|
assert [s["agent"] for s in out["steps"]] == ["finance", "finance", "office"]
|
|
121
116
|
assert out["steps"][1]["depends_on"] == [0]
|
|
122
117
|
|
|
123
|
-
# 单步 → flow 强制 single
|
|
124
118
|
r2 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
125
119
|
_plan_args("parallel", [{"name": "查天气", "agent": "office"}])]})
|
|
126
120
|
out2 = llm_plan(r2, "查天气", _catalog())
|
|
127
121
|
assert out2["flow"] == "single" and len(out2["steps"]) == 1
|
|
128
122
|
|
|
129
|
-
# 非法 agent(含已下放的 assistant/admin)兜底 office
|
|
130
123
|
r3 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
131
124
|
_plan_args("single", [{"name": "x", "agent": "不存在的Agent"}])]})
|
|
132
125
|
out3 = llm_plan(r3, "x", _catalog())
|
|
@@ -140,14 +133,12 @@ def test_supervisor_llm_plan():
|
|
|
140
133
|
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
141
134
|
sub = {"finance": D(), "office": D()}
|
|
142
135
|
|
|
143
|
-
# 未开启 → None
|
|
144
136
|
os.environ.pop("FOXCLAW_LLM_PLAN", None)
|
|
145
137
|
a1 = SupervisorAgent.__new__(SupervisorAgent)
|
|
146
138
|
a1.router = _FakeRouter(result={"supported": True})
|
|
147
139
|
a1._subagents = sub
|
|
148
140
|
assert a1._llm_plan("任务") is None
|
|
149
141
|
|
|
150
|
-
# 开启 + 多步 → (Plan, step_routes)
|
|
151
142
|
os.environ["FOXCLAW_LLM_PLAN"] = "1"
|
|
152
143
|
a2 = SupervisorAgent.__new__(SupervisorAgent)
|
|
153
144
|
a2.router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
@@ -161,14 +152,12 @@ def test_supervisor_llm_plan():
|
|
|
161
152
|
assert len(plan.tasks) == 2
|
|
162
153
|
assert routes == [("查支出", "finance"), ("发邮件", "office")]
|
|
163
154
|
|
|
164
|
-
# 单步 → None(交回关键词路由,不改变单步行为)
|
|
165
155
|
a3 = SupervisorAgent.__new__(SupervisorAgent)
|
|
166
156
|
a3.router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
167
157
|
_plan_args("single", [{"name": "查天气", "agent": "office"}])]})
|
|
168
158
|
a3._subagents = sub
|
|
169
159
|
assert a3._llm_plan("查天气") is None
|
|
170
160
|
|
|
171
|
-
# 不支持 tools → None
|
|
172
161
|
a4 = SupervisorAgent.__new__(SupervisorAgent)
|
|
173
162
|
a4.router = _FakeRouter(is_stub=True)
|
|
174
163
|
a4._subagents = sub
|
|
@@ -192,7 +181,6 @@ def test_llm_replan_parse():
|
|
|
192
181
|
print("\n【7】llm_replan:修正计划 + 空 steps 放弃 + fail-safe")
|
|
193
182
|
prior = [{"subtask": "查支出", "agent": "finance", "output": "超时", "blocked": True},
|
|
194
183
|
{"subtask": "发邮件", "agent": "office", "output": "未执行", "blocked": False}]
|
|
195
|
-
# 正常:换 agent 重排失败步骤
|
|
196
184
|
r = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
197
185
|
_plan_args("sequential", [
|
|
198
186
|
{"name": "查支出(重试)", "agent": "finance"},
|
|
@@ -200,12 +188,10 @@ def test_llm_replan_parse():
|
|
|
200
188
|
])]})
|
|
201
189
|
out = llm_replan(r, "查支出再发邮件", prior, _catalog())
|
|
202
190
|
assert out is not None and [s["agent"] for s in out["steps"]] == ["finance", "office"]
|
|
203
|
-
# 空 steps = 模型判定放弃 → 返回 {"steps": []} 而非 None
|
|
204
191
|
r2 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
205
192
|
_plan_args("sequential", [])]})
|
|
206
193
|
out2 = llm_replan(r2, "任务", prior, _catalog())
|
|
207
194
|
assert out2 is not None and out2["steps"] == []
|
|
208
|
-
# fail-safe:不支持 tools → None
|
|
209
195
|
r3 = _FakeRouter(result={"supported": False})
|
|
210
196
|
assert llm_replan(r3, "任务", prior, _catalog()) is None
|
|
211
197
|
print(" ✓ 修正计划 / 空steps放弃 / 不支持回退 正确")
|
|
@@ -214,12 +200,9 @@ def test_llm_replan_parse():
|
|
|
214
200
|
def test_dispatch_failed():
|
|
215
201
|
print("\n【8】_dispatch_failed:失败判定")
|
|
216
202
|
from foxclaw.agents.agents import _dispatch_failed
|
|
217
|
-
# 多步 blocked → True
|
|
218
203
|
assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": True}}) is True
|
|
219
|
-
# parallel steps 含 failed → True
|
|
220
204
|
assert _dispatch_failed({"result": {"dispatched_to": "parallel",
|
|
221
205
|
"parallel_steps": [{"failed": True}]}}) is True
|
|
222
|
-
# 单 Agent / 成功 / 非 dict → False
|
|
223
206
|
assert _dispatch_failed({"result": {"dispatched_to": "finance"}}) is False
|
|
224
207
|
assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": False,
|
|
225
208
|
"multi_steps": [{"blocked": False}]}}) is False
|
|
@@ -235,7 +218,6 @@ def test_replan_and_redo():
|
|
|
235
218
|
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
236
219
|
sub = {"finance": D(), "office": D()}
|
|
237
220
|
|
|
238
|
-
# 反思时返回修正计划(重试失败步骤)
|
|
239
221
|
router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
240
222
|
_plan_args("sequential", [
|
|
241
223
|
{"name": "查支出(重试)", "agent": "finance"},
|
|
@@ -244,7 +226,7 @@ def test_replan_and_redo():
|
|
|
244
226
|
a = SupervisorAgent.__new__(SupervisorAgent)
|
|
245
227
|
a.router = router
|
|
246
228
|
a._subagents = sub
|
|
247
|
-
a._think = lambda **kw: None
|
|
229
|
+
a._think = lambda **kw: None
|
|
248
230
|
|
|
249
231
|
calls = {"n": 0}
|
|
250
232
|
|
|
@@ -283,7 +265,6 @@ def test_replan_gives_up():
|
|
|
283
265
|
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
284
266
|
sub = {"finance": D(), "assistant": D()}
|
|
285
267
|
|
|
286
|
-
# 反思返回空 steps(放弃)
|
|
287
268
|
router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
288
269
|
_plan_args("sequential", [])]})
|
|
289
270
|
a = SupervisorAgent.__new__(SupervisorAgent)
|
|
@@ -314,21 +295,18 @@ def test_sv_replan_stage():
|
|
|
314
295
|
from types import SimpleNamespace
|
|
315
296
|
from foxclaw.pipeline.supervisor_stages import sv_replan
|
|
316
297
|
|
|
317
|
-
# 无失败 → 短路
|
|
318
298
|
a1 = SimpleNamespace(_replan_enabled=lambda: True)
|
|
319
299
|
ctx1 = SimpleNamespace(extra={}, agent=a1,
|
|
320
300
|
dispatch_result={"result": {"dispatched_to": "multi", "blocked": False}})
|
|
321
301
|
asyncio.run(sv_replan(ctx1))
|
|
322
302
|
assert ctx1.dispatch_result["result"]["blocked"] is False
|
|
323
303
|
|
|
324
|
-
# 有失败但未开启 → 短路保留原结果
|
|
325
304
|
a2 = SimpleNamespace(_replan_enabled=lambda: False)
|
|
326
305
|
ctx2 = SimpleNamespace(extra={}, agent=a2,
|
|
327
306
|
dispatch_result={"result": {"dispatched_to": "multi", "blocked": True}})
|
|
328
307
|
asyncio.run(sv_replan(ctx2))
|
|
329
308
|
assert ctx2.dispatch_result["result"]["blocked"] is True
|
|
330
309
|
|
|
331
|
-
# 触发路径:有失败 + 开启 → 调用 _replan_and_redo 更新结果
|
|
332
310
|
async def fake_replan(task, sched, session_id, subagents, context, memories, dr, max_rounds=2):
|
|
333
311
|
return {"result": {"dispatched_to": "multi", "blocked": False}}
|
|
334
312
|
|
|
@@ -16,58 +16,47 @@ def test_markdown_skills():
|
|
|
16
16
|
with TestClient(app) as c:
|
|
17
17
|
print("=" * 60); print("工具中心 · SKILL.md 指令技能 验证"); print("=" * 60)
|
|
18
18
|
|
|
19
|
-
# 1. 缺 ack → 409
|
|
20
19
|
r = c.post("/api/ahs/md-skills", json={"content": SKILL})
|
|
21
20
|
assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
|
|
22
21
|
print("[1] 未确认 → 409 needs_ack OK")
|
|
23
22
|
|
|
24
|
-
# 2. ack → 200,列表出现
|
|
25
23
|
r = c.post("/api/ahs/md-skills", json={"content": SKILL, "acknowledged": True})
|
|
26
24
|
assert r.status_code == 200, r.text
|
|
27
25
|
sid = r.json()["skill"]["id"]
|
|
28
26
|
metas = c.get("/api/ahs/md-skills").json()["skills"]
|
|
29
27
|
assert any(s["id"] == sid for s in metas)
|
|
30
|
-
assert all("body" not in s for s in metas)
|
|
28
|
+
assert all("body" not in s for s in metas)
|
|
31
29
|
print("[2] 上传(ack)+列表(不含body) OK")
|
|
32
30
|
|
|
33
|
-
# 3. GET/{id} 含 body
|
|
34
31
|
r = c.get(f"/api/ahs/md-skills/{sid}")
|
|
35
32
|
assert r.status_code == 200 and "成果" in r.json()["body"]
|
|
36
33
|
print("[3] 查看含 body OK")
|
|
37
34
|
|
|
38
|
-
# 4. 缺 name → 400
|
|
39
35
|
r = c.post("/api/ahs/md-skills", json={"content": "---\ndescription: x\n---\nbody", "acknowledged": True})
|
|
40
36
|
assert r.status_code == 400
|
|
41
37
|
print("[4] 缺 name → 400 OK")
|
|
42
38
|
|
|
43
|
-
# 5. 启停
|
|
44
39
|
assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": False}).json()["skill"]["enabled"] is False
|
|
45
40
|
assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": True}).status_code == 200
|
|
46
41
|
print("[5] 启停 OK")
|
|
47
42
|
|
|
48
|
-
# 6. 注入:直建 OS,agent 挂了 md_skills,_skill_augment 命中注入 + 持久化
|
|
49
43
|
from foxclaw import Foxclaw
|
|
50
44
|
os2 = Foxclaw(data_dir=_DATA)
|
|
51
45
|
assert os2.md_skills.get(sid) is not None, "重启后指令技能丢失(持久化)"
|
|
52
46
|
ag = os2.supervisor
|
|
53
47
|
out = ag._skill_augment("帮我写周报", [{"role": "user", "content": "帮我写周报"}])
|
|
54
48
|
assert "先列本周成果" in out[-1]["content"] and "帮我写周报" in out[-1]["content"]
|
|
55
|
-
assert sum(1 for i in range(len(out) - 1) if out[i]["role"] == out[i + 1]["role"] == "user") == 0
|
|
49
|
+
assert sum(1 for i in range(len(out) - 1) if out[i]["role"] == out[i + 1]["role"] == "user") == 0
|
|
56
50
|
out2 = ag._skill_augment("订机票", [{"role": "user", "content": "订机票"}])
|
|
57
|
-
assert out2[0]["content"] == "订机票"
|
|
51
|
+
assert out2[0]["content"] == "订机票"
|
|
58
52
|
print("[6] 注入命中/未命中 + 持久化 round-trip OK")
|
|
59
53
|
|
|
60
|
-
# 7. 删除
|
|
61
54
|
with TestClient(app) as c:
|
|
62
55
|
assert c.delete(f"/api/ahs/md-skills/{sid}").status_code == 200
|
|
63
56
|
assert not any(s["id"] == sid for s in c.get("/api/ahs/md-skills").json()["skills"])
|
|
64
57
|
print("[7] 删除 OK")
|
|
65
58
|
|
|
66
59
|
print("\nSKILL.md 指令技能验证全部通过")
|
|
67
|
-
# 注:OpenClaw 兼容段(原 [8]-[12])已拆到 test_markdown_skills_openclaw.py。
|
|
68
|
-
# 原因:单进程内 3×TestClient lifespan + 4×Foxclaw 实例 + 两个不同 ST 模型共存时,
|
|
69
|
-
# 第三次 lifespan 预热阶段 torch/FAISS 必现 0xC0000005 进程崩溃(基线对照实锤,
|
|
70
|
-
# 与业务改动无关)。进程隔离后两段各自全绿。
|
|
71
60
|
|
|
72
61
|
if __name__ == "__main__":
|
|
73
62
|
test_markdown_skills()
|
|
@@ -17,7 +17,6 @@ def test_markdown_skills_openclaw():
|
|
|
17
17
|
from fastapi.testclient import TestClient
|
|
18
18
|
from foxclaw.api import app
|
|
19
19
|
|
|
20
|
-
# 8. OpenClaw 兼容:无 triggers 的原味技能(单 .md)→ 装成功 + 描述相关触发
|
|
21
20
|
NO_TRIG = "---\nname: skill-vetter\nversion: 1.0.0\ndescription: Security-first skill vetting. Use before installing any skill from ClawdHub or GitHub.\n---\nCheck red flags before install."
|
|
22
21
|
with TestClient(app) as c:
|
|
23
22
|
r = c.post("/api/ahs/md-skills", json={"content": NO_TRIG, "id": "skill_vetter", "acknowledged": True})
|
|
@@ -32,37 +31,33 @@ def test_markdown_skills_openclaw():
|
|
|
32
31
|
z.writestr("_meta.json", _json.dumps({"slug": "skill-vetter-zip", "version": "1.0.0"}))
|
|
33
32
|
_pkg = _b64.b64encode(_buf.getvalue()).decode()
|
|
34
33
|
with TestClient(app) as c:
|
|
35
|
-
# 9. zip 包上传(base64)→ id 用 slug、version 带出
|
|
36
34
|
r = c.post("/api/ahs/md-skills", json={"package_b64": _pkg, "acknowledged": True})
|
|
37
35
|
assert r.status_code == 200, r.text
|
|
38
36
|
assert r.json()["skill"]["id"] == "skill-vetter-zip" and r.json()["skill"]["version"] == "1.0.0"
|
|
39
37
|
print("[9] OpenClaw zip 包上传(slug→id,version) OK")
|
|
40
|
-
# 10. zip 无 SKILL.md → 400
|
|
41
38
|
_b2 = _io.BytesIO()
|
|
42
39
|
with _zip.ZipFile(_b2, "w") as z: z.writestr("readme.txt", "hi")
|
|
43
40
|
r = c.post("/api/ahs/md-skills", json={"package_b64": _b64.b64encode(_b2.getvalue()).decode(), "acknowledged": True})
|
|
44
41
|
assert r.status_code == 400
|
|
45
42
|
print("[10] zip 无 SKILL.md → 400 OK")
|
|
46
43
|
|
|
47
|
-
# 11. 描述相关注入(无 triggers)+ 持久化 round-trip
|
|
48
44
|
from foxclaw import Foxclaw
|
|
49
45
|
os3 = Foxclaw(data_dir=_DATA)
|
|
50
|
-
assert os3.md_skills.get("skill-vetter-zip") is not None
|
|
46
|
+
assert os3.md_skills.get("skill-vetter-zip") is not None
|
|
51
47
|
ag3 = os3.supervisor
|
|
52
48
|
hit = ag3._skill_augment("please vet this skill before install", [{"role": "user", "content": "please vet this skill before install"}])
|
|
53
|
-
assert "Check red flags" in hit[-1]["content"]
|
|
49
|
+
assert "Check red flags" in hit[-1]["content"]
|
|
54
50
|
miss = ag3._skill_augment("订一张机票", [{"role": "user", "content": "订一张机票"}])
|
|
55
|
-
assert miss[0]["content"] == "订一张机票"
|
|
51
|
+
assert miss[0]["content"] == "订一张机票"
|
|
56
52
|
print("[11] 无 triggers 描述相关注入 + 持久化 OK")
|
|
57
53
|
|
|
58
|
-
# 12. 编辑触发词(免解压重打包):PATCH triggers → 中文任务也能触发
|
|
59
54
|
with TestClient(app) as c:
|
|
60
55
|
r = c.patch("/api/ahs/md-skills/skill-vetter-zip", json={"triggers": ["审核", "搜索"]})
|
|
61
56
|
assert r.status_code == 200 and r.json()["skill"]["triggers"] == ["审核", "搜索"], r.text
|
|
62
57
|
os4 = Foxclaw(data_dir=_DATA)
|
|
63
58
|
ag4 = os4.supervisor
|
|
64
59
|
hit = ag4._skill_augment("帮我审核这个技能", [{"role": "user", "content": "帮我审核这个技能"}])
|
|
65
|
-
assert "Check red flags" in hit[-1]["content"]
|
|
60
|
+
assert "Check red flags" in hit[-1]["content"]
|
|
66
61
|
print("[12] 编辑触发词 → 中文任务触发 OK")
|
|
67
62
|
|
|
68
63
|
print("\nOpenClaw 兼容验证全部通过")
|
|
@@ -5,8 +5,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
5
5
|
|
|
6
6
|
def test_mcp_connectors():
|
|
7
7
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
8
|
-
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
9
|
-
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
8
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
9
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
10
10
|
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
11
11
|
_DATA = tempfile.mkdtemp(prefix="foxclaw_mcp_")
|
|
12
12
|
os.environ["FOXCLAW_DATA_DIR"] = _DATA
|
|
@@ -16,8 +16,6 @@ def test_mcp_connectors():
|
|
|
16
16
|
import foxclaw.skills.mcp_client as mc
|
|
17
17
|
from types import SimpleNamespace as NS
|
|
18
18
|
|
|
19
|
-
# 客户端 SSRF 防线(用真实 _amcp_list_tools,monkeypatch 之前测):连内网/元数据 host
|
|
20
|
-
# 应被请求钩子在发包前拦截(防恶意 MCP server 用 307 把你重定向进内网/云元数据)。
|
|
21
19
|
import asyncio as _aio
|
|
22
20
|
_blocked = False
|
|
23
21
|
try:
|
|
@@ -42,46 +40,38 @@ def test_mcp_connectors():
|
|
|
42
40
|
with TestClient(app) as c:
|
|
43
41
|
print("=" * 60); print("工具中心 · MCP 连接器 验证"); print("=" * 60)
|
|
44
42
|
|
|
45
|
-
# 1. 加服务器(https, ack) → 注册 2 工具
|
|
46
43
|
r = c.post("/api/ahs/mcp/servers", json=SRV)
|
|
47
44
|
assert r.status_code == 200, r.text
|
|
48
45
|
assert r.json()["tools_registered"] == 2
|
|
49
46
|
print("[1] 加服务器 + 注册 2 工具 OK")
|
|
50
47
|
|
|
51
|
-
# 2. 技能出现 + fail-closed side_effect
|
|
52
48
|
skills = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
53
49
|
assert skills["mcp__demo_srv__get_time"]["side_effect"] == "read"
|
|
54
|
-
assert skills["mcp__demo_srv__do_thing"]["side_effect"] == "write"
|
|
50
|
+
assert skills["mcp__demo_srv__do_thing"]["side_effect"] == "write"
|
|
55
51
|
print("[2] 工具成技能 + fail-closed(read/write) OK")
|
|
56
52
|
|
|
57
|
-
# 3. 服务器列表 tool_count=2
|
|
58
53
|
srvs = c.get("/api/ahs/mcp/servers").json()["servers"]
|
|
59
54
|
assert srvs and srvs[0]["tool_count"] == 2
|
|
60
55
|
print("[3] 服务器列表 tool_count=2 OK")
|
|
61
56
|
|
|
62
|
-
# 4. 审核门 deny 内网
|
|
63
57
|
r = c.post("/api/ahs/mcp/servers", json={"id": "bad", "url": "http://127.0.0.1/mcp"})
|
|
64
58
|
assert r.status_code == 403, r.text
|
|
65
59
|
print("[4] 审核门 deny 内网(403) OK")
|
|
66
60
|
|
|
67
|
-
# 5. 外部低信任未 ack → 409
|
|
68
61
|
r = c.post("/api/ahs/mcp/servers", json={"id": "p2", "url": "https://mcp2.example.com/mcp"})
|
|
69
62
|
assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
|
|
70
63
|
print("[5] 外部低信任需确认(409) OK")
|
|
71
64
|
|
|
72
|
-
# 6. 测试调用 mcp 技能:external 外部调用 → 先 needs_confirmation,确认后成功
|
|
73
65
|
r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}})
|
|
74
66
|
assert r.json()["status"] == "needs_confirmation", r.text
|
|
75
67
|
r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}, "confirmed": True})
|
|
76
68
|
j = r.json()
|
|
77
69
|
assert r.status_code == 200 and j["success"] is True, r.text
|
|
78
|
-
# 双层防提示注入:结构标记 _untrusted + 内容内联安全警告(LLM 注入时看得到)
|
|
79
70
|
out = j["output"]
|
|
80
71
|
assert out.get("_untrusted") is True, out
|
|
81
72
|
assert "不可信" in out.get("data", {}).get("content", ""), out
|
|
82
73
|
print("[6] 测试调用 mcp 技能 + 双层 untrusted 标记(防提示注入) OK")
|
|
83
74
|
|
|
84
|
-
# 7. 连接失败 → 502
|
|
85
75
|
async def boom(url, headers, transport): raise RuntimeError("connrefused")
|
|
86
76
|
mc._amcp_list_tools = boom
|
|
87
77
|
r = c.post("/api/ahs/mcp/servers", json={"id": "err_srv", "url": "https://mcp3.example.com/mcp", "acknowledged": True})
|
|
@@ -89,12 +79,10 @@ def test_mcp_connectors():
|
|
|
89
79
|
mc._amcp_list_tools = fake_list
|
|
90
80
|
print("[7] 连接失败 502 OK")
|
|
91
81
|
|
|
92
|
-
# 8. 移除服务器 → 技能消失
|
|
93
82
|
assert c.delete("/api/ahs/mcp/servers/demo_srv").status_code == 200
|
|
94
83
|
assert "mcp__demo_srv__get_time" not in {s["id"] for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
95
84
|
print("[8] 移除服务器 + 技能消失 OK")
|
|
96
85
|
|
|
97
|
-
# 9. 持久化 round-trip
|
|
98
86
|
with TestClient(app) as c:
|
|
99
87
|
c.post("/api/ahs/mcp/servers", json=SRV)
|
|
100
88
|
from foxclaw import Foxclaw
|
|
@@ -8,17 +8,15 @@ def test_mcp_guardian_discovery():
|
|
|
8
8
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
9
9
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_mcp_guardian_")
|
|
10
10
|
|
|
11
|
-
# 1. tool_to_spec 兼容 dict(守门人序列化产物),fail-closed 不变
|
|
12
11
|
from foxclaw.skills.mcp_client import tool_to_spec
|
|
13
12
|
spec = tool_to_spec("srv", {"name": "get_time", "description": "读时间", "inputSchema": {},
|
|
14
13
|
"annotations": {"readOnlyHint": True, "destructiveHint": None}})
|
|
15
14
|
assert spec.id == "mcp__srv__get_time" and spec.side_effect == "read" and not spec.destructive
|
|
16
15
|
spec2 = tool_to_spec("srv", {"name": "do_thing", "description": "", "inputSchema": {},
|
|
17
16
|
"annotations": None})
|
|
18
|
-
assert spec2.side_effect == "write"
|
|
17
|
+
assert spec2.side_effect == "write"
|
|
19
18
|
print("[1] tool_to_spec 兼容 dict + fail-closed OK")
|
|
20
19
|
|
|
21
|
-
# 2. 守门人 mcp_register:主进程空 headers 不覆盖守门人已有凭证
|
|
22
20
|
import foxclaw.kernel.sentinel as sm
|
|
23
21
|
servers = {}
|
|
24
22
|
sm._handle_mcp_register({"servers": [
|
|
@@ -30,7 +28,6 @@ def test_mcp_guardian_discovery():
|
|
|
30
28
|
assert servers["a"]["headers"]["Authorization"] == "Bearer SECRET", "空 headers 不应覆盖守门人已有凭证"
|
|
31
29
|
print("[2] mcp_register 空 headers 保留旧凭证 OK")
|
|
32
30
|
|
|
33
|
-
# 3. 守门人 mcp_list_tools 代发现:用自己的 headers 连 + 序列化为 dict
|
|
34
31
|
import foxclaw.skills.mcp_client as mc
|
|
35
32
|
_seen = {}
|
|
36
33
|
async def fake_list(url, headers, transport):
|
|
@@ -41,10 +38,9 @@ def test_mcp_guardian_discovery():
|
|
|
41
38
|
out = sm._handle_mcp_list_tools({"server_id": "a"}, servers)
|
|
42
39
|
assert out["tools"][0]["name"] == "t1"
|
|
43
40
|
assert out["tools"][0]["annotations"]["readOnlyHint"] is True
|
|
44
|
-
assert _seen["headers"].get("Authorization") == "Bearer SECRET"
|
|
41
|
+
assert _seen["headers"].get("Authorization") == "Bearer SECRET"
|
|
45
42
|
print("[3] mcp_list_tools 守门人代发现 + 序列化 + 凭证不出守门人 OK")
|
|
46
43
|
|
|
47
|
-
# 4. mcp_add_server_guardian:主进程不存 headers,守门人收到 headers
|
|
48
44
|
class FakeSentinel:
|
|
49
45
|
def __init__(self):
|
|
50
46
|
self.registered = None
|
|
@@ -6,9 +6,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
6
6
|
|
|
7
7
|
def test_mcp_no_memory():
|
|
8
8
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
9
|
-
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
9
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
10
10
|
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
11
|
-
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
11
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
12
12
|
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
13
13
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_nomem_")
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ def test_mcp_no_memory():
|
|
|
16
16
|
|
|
17
17
|
o = Foxclaw(data_dir=os.environ["FOXCLAW_DATA_DIR"])
|
|
18
18
|
calls = []
|
|
19
|
-
o.hmr.ingest = lambda *a, **k: calls.append(1)
|
|
19
|
+
o.hmr.ingest = lambda *a, **k: calls.append(1)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
async def dispatch_suppressed():
|
|
@@ -24,7 +24,6 @@ class _StubGate:
|
|
|
24
24
|
taint_level=0, taint_threshold=0):
|
|
25
25
|
assert skill.side_effect == "write" and skill.external is True
|
|
26
26
|
assert confirmed is False, "外部驱动不能用 confirmed 自证"
|
|
27
|
-
# Verdict 的字段是 level(allow|warn|deny|confirm),不是 decision
|
|
28
27
|
return SimpleNamespace(level=self.decision, reason="stub",
|
|
29
28
|
raw=SimpleNamespace(reason_code=self.code),
|
|
30
29
|
decision_id="d")
|