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
|
@@ -19,67 +19,56 @@ def test_tool_center():
|
|
|
19
19
|
with TestClient(app) as c:
|
|
20
20
|
print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
|
|
21
21
|
|
|
22
|
-
# 1. 安装 web_api 技能(https 只读 → allow)
|
|
23
22
|
r = c.post("/api/ahs/skills", json=WEBAPI)
|
|
24
23
|
assert r.status_code == 200, r.text
|
|
25
24
|
assert r.json()["created"] is True
|
|
26
25
|
print("[1] 安装 web_api 技能 OK")
|
|
27
26
|
|
|
28
|
-
# 2. 列表里出现且 source=user
|
|
29
27
|
r = c.get("/api/ahs/skills"); assert r.status_code == 200
|
|
30
28
|
ids = {s["id"]: s for s in r.json()["skills"]}
|
|
31
29
|
assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
|
|
32
30
|
print("[2] 列表含新技能且 source=user OK")
|
|
33
31
|
|
|
34
|
-
# 3. 拒绝 local kind
|
|
35
32
|
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
|
|
36
33
|
assert r.status_code == 400
|
|
37
34
|
print("[3] 拒绝 local kind(400) OK")
|
|
38
35
|
|
|
39
|
-
# 4. 拒绝非法 endpoint
|
|
40
36
|
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
|
|
41
37
|
assert r.status_code == 400
|
|
42
38
|
print("[4] 拒非法 endpoint(400) OK")
|
|
43
39
|
|
|
44
|
-
# 5. mcp 缺字段
|
|
45
40
|
r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
|
|
46
41
|
assert r.status_code == 400
|
|
47
42
|
print("[5] mcp 缺 server/tool(400) OK")
|
|
48
43
|
|
|
49
|
-
# 6. 启停
|
|
50
44
|
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
|
|
51
45
|
assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
|
|
52
46
|
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
|
|
53
47
|
assert r.json()["skill"]["enabled"] is True
|
|
54
48
|
print("[6] 启停切换 OK")
|
|
55
49
|
|
|
56
|
-
# 7. 删内置 → 409(取列表里第一个 builtin)
|
|
57
50
|
r = c.get("/api/ahs/skills")
|
|
58
51
|
builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
|
|
59
52
|
if builtin:
|
|
60
53
|
rd = c.delete(f"/api/ahs/skills/{builtin}")
|
|
61
54
|
assert rd.status_code == 409, rd.text
|
|
62
55
|
print("[7] 内置技能不可删(409) OK")
|
|
63
|
-
# 7b. 用内置 id 安装 → 拒(不能覆盖内置)
|
|
64
56
|
rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
|
|
65
57
|
assert rc.status_code == 409, rc.text
|
|
66
58
|
print("[7b] 用内置 id 安装被拒(409) OK")
|
|
67
59
|
else:
|
|
68
60
|
print("[7] 跳过(无内置技能)")
|
|
69
61
|
|
|
70
|
-
# 8. 测试调用:read 技能直接跑(返回 SkillResult 结构)
|
|
71
62
|
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
|
|
72
63
|
assert r.status_code == 200 and "success" in r.json()
|
|
73
64
|
print("[8] 测试调用返回 SkillResult OK")
|
|
74
65
|
|
|
75
|
-
# 9. 安装审核门:打本地地址的 web_api 安装期被 deny(403)
|
|
76
66
|
r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
|
|
77
67
|
"triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
|
|
78
68
|
assert r.status_code == 403, r.text
|
|
79
69
|
assert r.json()["detail"]["error"] == "install_denied"
|
|
80
70
|
print("[9] 安装审核门 deny 本地地址(403) OK")
|
|
81
71
|
|
|
82
|
-
# 9b. 有副作用技能未确认 → 409 needs_ack;acknowledged 后 → 200(且持久,供步12 round-trip)
|
|
83
72
|
_send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
|
|
84
73
|
"triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
|
|
85
74
|
r = c.post("/api/ahs/skills", json=_send)
|
|
@@ -88,19 +77,16 @@ def test_tool_center():
|
|
|
88
77
|
assert r.status_code == 200, r.text
|
|
89
78
|
print("[9b] 副作用技能需确认(409→200) OK")
|
|
90
79
|
|
|
91
|
-
# 10. 治理端点可用
|
|
92
80
|
assert c.get("/api/ahs/breaker").status_code == 200
|
|
93
81
|
assert c.get("/api/ahs/hooks/audit").status_code == 200
|
|
94
82
|
print("[10] 治理端点(breaker/audit) OK")
|
|
95
83
|
|
|
96
|
-
# 10b. 信任分级:内置 calculator(local)=high、内置 mcp=low、用户 web_api=mid
|
|
97
84
|
sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
98
85
|
assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
|
|
99
|
-
assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
|
|
86
|
+
assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
|
|
100
87
|
assert sk["wx_weather"]["trust"] == "mid"
|
|
101
88
|
print("[10b] 信任分级 high/mid/low OK")
|
|
102
89
|
|
|
103
|
-
# 10c. deny 封杀:封杀后测试调用被拦 + blocked 标;解封恢复
|
|
104
90
|
c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
|
|
105
91
|
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
|
|
106
92
|
assert r["status"] == "blocked" and "封杀" in r["message"], r
|
|
@@ -111,12 +97,10 @@ def test_tool_center():
|
|
|
111
97
|
assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
|
|
112
98
|
print("[10c] deny 封杀/解封 kill-switch OK")
|
|
113
99
|
|
|
114
|
-
# 11. 删除用户技能
|
|
115
100
|
assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
|
|
116
101
|
r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
|
|
117
102
|
print("[11] 删除用户技能 OK")
|
|
118
103
|
|
|
119
|
-
# 12. 持久化 round-trip:直建新 OS(同数据目录),已安装的 send_x 应仍在
|
|
120
104
|
from foxclaw import Foxclaw
|
|
121
105
|
os2 = Foxclaw(data_dir=_DATA)
|
|
122
106
|
assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"
|
|
@@ -43,7 +43,7 @@ def test_readonly_tools_run_concurrently(monkeypatch) -> None:
|
|
|
43
43
|
barrier = threading.Barrier(2, timeout=3)
|
|
44
44
|
|
|
45
45
|
def behave(name, args, sink):
|
|
46
|
-
barrier.wait()
|
|
46
|
+
barrier.wait()
|
|
47
47
|
return f"{name}:ok"
|
|
48
48
|
|
|
49
49
|
fake = _make(monkeypatch, behave)
|
|
@@ -72,7 +72,7 @@ def test_slow_tool_is_cut_off(monkeypatch) -> None:
|
|
|
72
72
|
|
|
73
73
|
def test_taint_collected_in_worker_and_merged_in_main(monkeypatch) -> None:
|
|
74
74
|
def behave(name, args, sink):
|
|
75
|
-
sink.append(3 if name == "web_search" else 2)
|
|
75
|
+
sink.append(3 if name == "web_search" else 2)
|
|
76
76
|
return "ok"
|
|
77
77
|
|
|
78
78
|
fake = _make(monkeypatch, behave)
|
|
@@ -46,13 +46,11 @@ def main():
|
|
|
46
46
|
print("\n【3】类型不符被检出(C 类:静默失败改为可见)")
|
|
47
47
|
errs = r.validate_args("web_search", {"query": {"a": 1}})
|
|
48
48
|
check(any("类型" in e for e in errs), f"dict 传 string 位被检出({errs})")
|
|
49
|
-
# top_k 声明在 recall_memory 上(不在 web_search),故用它验 integer 校验
|
|
50
49
|
errs = r.validate_args("recall_memory", {"query": "x", "top_k": "ten"})
|
|
51
50
|
check(any("类型" in e for e in errs), f"字符串传 integer 被检出({errs})")
|
|
52
51
|
errs = r.validate_args("recall_memory", {"query": "x", "top_k": True})
|
|
53
52
|
check(not any("top_k" in e for e in errs),
|
|
54
|
-
f"bool 不被误判为 integer({errs})")
|
|
55
|
-
# 未在 schema 中声明的额外字段应放过(不引入 jsonschema 依赖的宽松设计)
|
|
53
|
+
f"bool 不被误判为 integer({errs})")
|
|
56
54
|
check(r.validate_args("web_search", {"query": "ok", "未声明字段": 1}) == [],
|
|
57
55
|
"未声明的额外字段不报错(避免过度约束模型)")
|
|
58
56
|
|
|
@@ -82,8 +80,6 @@ def main():
|
|
|
82
80
|
print("\n【6】参数解析失败不再静默(args_parse_failed 标记存在)")
|
|
83
81
|
import inspect
|
|
84
82
|
from foxclaw.core.model_router import ModelRouter
|
|
85
|
-
# 检查整个类:标记可能随重构从 call_tools 移到 _call_openai_tools 等子方法,
|
|
86
|
-
# 锁「类内存在该标记」而非锁某个方法名(避免重构后误报)。
|
|
87
83
|
src = inspect.getsource(ModelRouter)
|
|
88
84
|
check("args_parse_failed" in src,
|
|
89
85
|
"call_tools 标记解析失败(可观测)")
|
|
@@ -20,7 +20,6 @@ from foxclaw.core.model_router import ModelRouter
|
|
|
20
20
|
from foxclaw.agents.base import CognitiveAgent
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
# ── 1. P1 运行时注入 ───────────────────────────────────────────────────────────
|
|
24
23
|
def test_runtime_context_and_idempotent():
|
|
25
24
|
r = ModelRouter(provider="stub")
|
|
26
25
|
ctx = r._runtime_context()
|
|
@@ -30,7 +29,6 @@ def test_runtime_context_and_idempotent():
|
|
|
30
29
|
assert r._inject_runtime(a) == a, "二次注入必须幂等(防 stream→complete 重复)"
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
# ── 2. ToolRegistry ────────────────────────────────────────────────────────────
|
|
34
32
|
def test_registry_default_and_deny():
|
|
35
33
|
reg = T.default_registry()
|
|
36
34
|
assert set(reg.names()) == {"web_search", "recall_memory"}
|
|
@@ -55,7 +53,6 @@ def test_tool_use_flag_default_off():
|
|
|
55
53
|
os.environ.pop("FOXCLAW_TOOL_USE", None)
|
|
56
54
|
|
|
57
55
|
|
|
58
|
-
# ── 3. call_tools 降级 ─────────────────────────────────────────────────────────
|
|
59
56
|
def test_call_tools_unsupported_degrades():
|
|
60
57
|
r = ModelRouter(provider="stub")
|
|
61
58
|
assert r.supports_tools() is False
|
|
@@ -63,7 +60,6 @@ def test_call_tools_unsupported_degrades():
|
|
|
63
60
|
assert res["supported"] is False and res["tool_calls"] == []
|
|
64
61
|
|
|
65
62
|
|
|
66
|
-
# ── 4. 工具循环 ────────────────────────────────────────────────────────────────
|
|
67
63
|
class _FakeRouter:
|
|
68
64
|
"""脚本化 call_tools:按序返回预设结果,记录每轮 messages 长度。"""
|
|
69
65
|
def __init__(self, script):
|
|
@@ -93,10 +89,8 @@ def _fake_agent(router):
|
|
|
93
89
|
|
|
94
90
|
def test_reply_with_tools_multi_round():
|
|
95
91
|
router = _FakeRouter([
|
|
96
|
-
# 第1轮:模型要求调 web_search
|
|
97
92
|
{"supported": True, "text": "", "assistant_msg": {"role": "assistant", "content": ""},
|
|
98
93
|
"tool_calls": [{"id": "c1", "name": "web_search", "args": {"query": "最新模型"}}]},
|
|
99
|
-
# 第2轮:拿到结果后给最终答案
|
|
100
94
|
{"supported": True, "text": "根据搜索,最新是 X。", "tool_calls": [],
|
|
101
95
|
"assistant_msg": {"role": "assistant", "content": "根据搜索,最新是 X。"}},
|
|
102
96
|
])
|
|
@@ -104,7 +98,6 @@ def test_reply_with_tools_multi_round():
|
|
|
104
98
|
out = CognitiveAgent._reply_with_tools(agent, "有哪些新模型",
|
|
105
99
|
[{"role": "user", "content": "有哪些新模型"}])
|
|
106
100
|
assert out == "根据搜索,最新是 X。"
|
|
107
|
-
# 第2轮的 messages 必须已追加 assistant(带 tool_calls)+ tool 结果
|
|
108
101
|
_, round2_msgs = router.seen[1]
|
|
109
102
|
roles = [m["role"] for m in round2_msgs]
|
|
110
103
|
assert "assistant" in roles and "tool" in roles
|
|
@@ -129,7 +122,6 @@ def test_reply_with_tools_unsupported_returns_none():
|
|
|
129
122
|
|
|
130
123
|
|
|
131
124
|
def test_reply_with_tools_empty_answer_returns_none():
|
|
132
|
-
# 阶段五·步骤15:模型无工具调用且文本为空 → 返回 None 让上层 fallback(不再"空回复且不回退")
|
|
133
125
|
router = _FakeRouter([
|
|
134
126
|
{"supported": True, "text": "", "tool_calls": [],
|
|
135
127
|
"assistant_msg": {"role": "assistant", "content": ""}},
|
|
@@ -139,7 +131,6 @@ def test_reply_with_tools_empty_answer_returns_none():
|
|
|
139
131
|
assert out is None, "空答必须返回 None 让上层 fallback"
|
|
140
132
|
|
|
141
133
|
|
|
142
|
-
# ── 5. 工具分派 ────────────────────────────────────────────────────────────────
|
|
143
134
|
def test_dispatch_web_search_ok(monkeypatch):
|
|
144
135
|
from foxclaw.core import web_search as ws
|
|
145
136
|
monkeypatch.setattr(ws, "search_answer", lambda q: {"answer": "实时答案XYZ", "references": []})
|
|
@@ -168,11 +159,9 @@ def test_dispatch_denied(monkeypatch):
|
|
|
168
159
|
assert "禁用" in out
|
|
169
160
|
|
|
170
161
|
|
|
171
|
-
# ── 6. 动作级权限闸(permission 管线 + HITL)──────────────────────────────────────
|
|
172
162
|
def test_tool_capability_readonly_and_failclosed():
|
|
173
163
|
assert T.tool_capability("web_search")["read_only"] is True
|
|
174
164
|
assert T.tool_capability("recall_memory")["side_effect"] == "read"
|
|
175
|
-
# 未声明的新工具 → fail-closed:当有副作用 + 外部
|
|
176
165
|
cap = T.tool_capability("some_future_write_tool")
|
|
177
166
|
assert cap["read_only"] is False and cap["side_effect"] == "write" and cap["external"] is True
|
|
178
167
|
|
|
@@ -194,7 +183,6 @@ def test_dispatch_hitl_blocks_side_effect_tool(monkeypatch):
|
|
|
194
183
|
called = {"n": 0}
|
|
195
184
|
monkeypatch.setattr(ws, "search_answer",
|
|
196
185
|
lambda q: called.__setitem__("n", called["n"] + 1) or {"answer": "x"})
|
|
197
|
-
# 把 web_search 强制降级为有副作用 → 应被闸门拦成 HITL
|
|
198
186
|
monkeypatch.setattr(T, "tool_capability",
|
|
199
187
|
lambda name: {"side_effect": "send", "read_only": False,
|
|
200
188
|
"external": True, "destructive": False})
|
|
@@ -219,7 +207,6 @@ def test_tool_path_injects_preferences(monkeypatch):
|
|
|
219
207
|
fake.AGENT_ID = "assistant"
|
|
220
208
|
fake._ability_augment = lambda t, m: m
|
|
221
209
|
fake._skill_augment = lambda t, m: m
|
|
222
|
-
# 偏好注入:把标记并进最后一条 user
|
|
223
210
|
fake._pref_augment = lambda m: (list(m[:-1]) +
|
|
224
211
|
[{"role": "user", "content": "【用户偏好】喜欢简洁\n\n" + m[-1]["content"]}])
|
|
225
212
|
|
|
@@ -70,7 +70,6 @@ def test_same_instance_concurrent(os_):
|
|
|
70
70
|
per[sid] = per.get(sid, 0) + 1
|
|
71
71
|
cross = sum(1 for _id, sid, p in rows for par in json.loads(p)
|
|
72
72
|
if par in sid_of and sid_of[par] != sid)
|
|
73
|
-
# goal 归属:每个返回会话的 goal 节点含本任务标记
|
|
74
73
|
ok_goal = 0
|
|
75
74
|
for i, r in enumerate(res):
|
|
76
75
|
g = con.execute("SELECT content FROM thought_nodes WHERE session_id=? "
|
|
@@ -90,9 +89,9 @@ def test_supervisor_concurrent(os_):
|
|
|
90
89
|
|
|
91
90
|
async def run():
|
|
92
91
|
return await asyncio.gather(
|
|
93
|
-
sup.run_task("分析本月理财预算"),
|
|
94
|
-
sup.run_task("总结这篇论文的要点"),
|
|
95
|
-
sup.run_task("你能做什么"),
|
|
92
|
+
sup.run_task("分析本月理财预算"),
|
|
93
|
+
sup.run_task("总结这篇论文的要点"),
|
|
94
|
+
sup.run_task("你能做什么"),
|
|
96
95
|
)
|
|
97
96
|
res = asyncio.run(run())
|
|
98
97
|
routed = [r["result"].get("dispatched_to") for r in res]
|
|
@@ -5,9 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
6
|
|
|
7
7
|
def test_tracing():
|
|
8
|
-
# 关掉 .env:否则开发者 .env 的 FOXCLAW_MCP_SERVER=1 会在 import api 时构建 MCP 服务器,
|
|
9
|
-
# 而本测试有**两处** `with TestClient(app)`(82/189 行),第二次进 lifespan 会撞
|
|
10
|
-
# StreamableHTTPSessionManager「.run() can only be called once per instance」而失败。
|
|
11
8
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
12
9
|
os.environ["HKC_EMBEDDING"] = "stub"
|
|
13
10
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
@@ -20,27 +17,24 @@ def test_tracing():
|
|
|
20
17
|
|
|
21
18
|
print("=" * 60); print("请求链路追踪验证"); print("=" * 60)
|
|
22
19
|
|
|
23
|
-
# [1] 无 trace 时 span 是零成本 no-op(后台任务不该报错)
|
|
24
20
|
clear_trace()
|
|
25
21
|
assert current_trace() is None
|
|
26
22
|
with span("orphan", x=1) as s:
|
|
27
23
|
assert s is None
|
|
28
24
|
print("[1] 无 trace 时 span no-op(不报错/不产生垃圾)OK")
|
|
29
25
|
|
|
30
|
-
# [2] span 记录名称/耗时/meta;嵌套按结束顺序落盘
|
|
31
26
|
tr = start_trace("unit")
|
|
32
27
|
with span("outer", k="v"):
|
|
33
28
|
with span("inner"):
|
|
34
29
|
time.sleep(0.01)
|
|
35
30
|
d = tr.to_dict()
|
|
36
31
|
names = [s["name"] for s in d["spans"]]
|
|
37
|
-
assert names == ["inner", "outer"], names
|
|
32
|
+
assert names == ["inner", "outer"], names
|
|
38
33
|
inner = d["spans"][0]
|
|
39
34
|
assert inner["ok"] is True and inner["ms"] >= 9, inner
|
|
40
35
|
assert d["spans"][1]["meta"] == {"k": "v"}
|
|
41
36
|
print(f"[2] span 记录(嵌套/耗时 {inner['ms']}ms/meta)OK")
|
|
42
37
|
|
|
43
|
-
# [3] 异常:记 ok=False + error,并原样抛出(不吞异常)
|
|
44
38
|
try:
|
|
45
39
|
with span("boom"):
|
|
46
40
|
raise ValueError("kaboom")
|
|
@@ -51,14 +45,12 @@ def test_tracing():
|
|
|
51
45
|
assert bad["ok"] is False and "kaboom" in bad["error"], bad
|
|
52
46
|
print("[3] span 异常记录 ok=False + 原样抛出 OK")
|
|
53
47
|
|
|
54
|
-
# [4] 单 trace span 上限(防病态循环撑爆内存)
|
|
55
48
|
t2 = Trace("cap")
|
|
56
49
|
for i in range(MAX_SPANS_PER_TRACE + 50):
|
|
57
50
|
t2.add_span({"name": f"s{i}"})
|
|
58
51
|
assert len(t2.to_dict()["spans"]) == MAX_SPANS_PER_TRACE and t2.to_dict()["dropped_spans"] == 50
|
|
59
52
|
print(f"[4] 单 trace span 封顶 {MAX_SPANS_PER_TRACE}(超出计 dropped)OK")
|
|
60
53
|
|
|
61
|
-
# [5] TraceStore 有界环存 + 按 id 取
|
|
62
54
|
st = TraceStore(limit=3)
|
|
63
55
|
ids = []
|
|
64
56
|
for i in range(5):
|
|
@@ -67,7 +59,6 @@ def test_tracing():
|
|
|
67
59
|
assert st.get(ids[-1]) is not None and st.get(ids[0]) is None, "应只留最近 3 条"
|
|
68
60
|
print("[5] TraceStore 有界环存(只留最近 N)+ 按 id 取 OK")
|
|
69
61
|
|
|
70
|
-
# [6] 线程隔离:contextvars 各线程独立,span 不串 trace
|
|
71
62
|
clear_trace()
|
|
72
63
|
res = {}
|
|
73
64
|
def worker(name):
|
|
@@ -81,13 +72,10 @@ def test_tracing():
|
|
|
81
72
|
assert all(v[1] == ["work"] for v in res.values()), "各线程 span 应各归各的 trace"
|
|
82
73
|
print("[6] 线程隔离(3 线程 3 条独立 trace,span 不串)OK")
|
|
83
74
|
|
|
84
|
-
# ── 端到端:经 API 请求产生 trace ─────────────────────────
|
|
85
75
|
from fastapi.testclient import TestClient
|
|
86
76
|
from foxclaw.api import app
|
|
87
77
|
|
|
88
78
|
with TestClient(app) as c:
|
|
89
|
-
# [7] 数据面请求 → 带 X-Trace-Id + 可查详情;链路里能看到 LLM span
|
|
90
|
-
# (用 office:主管走路由管线,不经过 act/verify,埋点覆盖要用领域 Agent 验)
|
|
91
79
|
r = c.post("/api/task", json={"task": "追踪自测", "agent_id": "office"})
|
|
92
80
|
assert r.status_code == 200, r.status_code
|
|
93
81
|
tid = r.headers.get("X-Trace-Id")
|
|
@@ -97,12 +85,9 @@ def test_tracing():
|
|
|
97
85
|
names = [s["name"] for s in detail["spans"]]
|
|
98
86
|
llm = [n for n in names if n.startswith("llm.")]
|
|
99
87
|
assert llm, f"链路里应有 LLM span: {names}"
|
|
100
|
-
# 跨层传播:Agent 跑在 asyncio.to_thread 的线程里,它的 span 仍落进本请求的 trace
|
|
101
|
-
# → 证明 contextvars 确实穿过了线程边界(本设计的核心假设)
|
|
102
88
|
mem = [n for n in names if n.startswith("memory.")]
|
|
103
89
|
assert mem, f"跨线程的 Agent 记忆 span 应归属本 trace(contextvars 传播): {names}"
|
|
104
90
|
assert all(s["ms"] >= 0 for s in detail["spans"]), "每个 span 都应有耗时"
|
|
105
|
-
# 覆盖度:认知管线各 stage 都要在链路里(否则"慢在哪"会落在没埋点的暗区)
|
|
106
91
|
stages = [n for n in names if n.startswith("stage.")]
|
|
107
92
|
for must in ("stage.recall", "stage.act", "stage.verify"):
|
|
108
93
|
assert must in names, f"缺 {must},埋点覆盖退化了: {names}"
|
|
@@ -110,21 +95,18 @@ def test_tracing():
|
|
|
110
95
|
print(f"[7] 请求→trace + X-Trace-Id;{detail['span_count']} span "
|
|
111
96
|
f"(管线 {len(stages)} stage + 跨线程 {mem[0]})OK")
|
|
112
97
|
|
|
113
|
-
# [8] 列表可查;未知 id → 404
|
|
114
98
|
lst = c.get("/api/traces?limit=5").json()["traces"]
|
|
115
99
|
assert any(t["trace_id"] == tid for t in lst), "最近列表应含刚才那条"
|
|
116
100
|
assert "spans" not in lst[0], "列表是摘要,不带 spans(省负载)"
|
|
117
101
|
assert c.get("/api/traces/deadbeef0000").status_code == 404
|
|
118
102
|
print(f"[8] /api/traces 列表({len(lst)} 条摘要)+ 未知 id 404 OK")
|
|
119
103
|
|
|
120
|
-
# [9] 探针/静态不追踪(不污染)
|
|
121
104
|
before = len(c.get("/api/traces?limit=200").json()["traces"])
|
|
122
105
|
c.get("/health"); c.get("/metrics")
|
|
123
106
|
after = c.get("/api/traces?limit=200").json()["traces"]
|
|
124
107
|
assert not any(t["name"].endswith("/health") for t in after), "探针不该被追踪"
|
|
125
108
|
print("[9] 探针 /health、/metrics 不进 trace OK")
|
|
126
109
|
|
|
127
|
-
# ── 持久化 + 采样(内存环重启即失;慢的/出错的必须留得住)──────────
|
|
128
110
|
import time as _t
|
|
129
111
|
store = TraceStore(limit=2)
|
|
130
112
|
store.attach_db(Path(os.environ["FOXCLAW_DATA_DIR"]) / "t.db")
|
|
@@ -134,7 +116,6 @@ def test_tracing():
|
|
|
134
116
|
tr.duration_ms = ms; tr.start_ts = _t.time()
|
|
135
117
|
return tr
|
|
136
118
|
|
|
137
|
-
# [10] 采样:慢的必存、出错的必存、正常的(采样率 0)不存
|
|
138
119
|
os.environ["FOXCLAW_TRACE_SLOW_MS"] = "500"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
|
|
139
120
|
slow = _mk("GET /slow", 900); fast = _mk("GET /fast", 5); bad = _mk("GET /err", 5, err=True)
|
|
140
121
|
for t in (slow, fast, bad):
|
|
@@ -145,21 +126,18 @@ def test_tracing():
|
|
|
145
126
|
assert fast.trace_id not in ids, "正常请求(采样率0)不该落库"
|
|
146
127
|
print("[10] 采样策略(慢/出错必存、正常按采样率不存)OK")
|
|
147
128
|
|
|
148
|
-
|
|
149
|
-
for i in range(5): # 环 limit=2,把 slow 挤出内存
|
|
129
|
+
for i in range(5):
|
|
150
130
|
store.put(_mk(f"GET /x{i}", 1))
|
|
151
131
|
assert store.get(slow.trace_id) is None, "应已滚出内存环"
|
|
152
132
|
d = store.get_dict(slow.trace_id)
|
|
153
133
|
assert d and d.get("persisted") and d["duration_ms"] == 900 and d["spans"], d
|
|
154
134
|
print("[11] 内存环滚出后仍可从库捞回(重启/滚动后可查历史)OK")
|
|
155
135
|
|
|
156
|
-
# [12] 过滤:按慢阈值 / 只看出错
|
|
157
136
|
assert all(r["duration_ms"] >= 500 for r in store.query(limit=99, slow_ms=500))
|
|
158
137
|
errs = store.query(limit=99, errors_only=True)
|
|
159
138
|
assert errs and all(r["ok"] is False for r in errs), errs
|
|
160
139
|
print("[12] 历史查询过滤(slow_ms / errors_only)OK")
|
|
161
140
|
|
|
162
|
-
# [13] 保留上限:超出删最旧(磁盘有界)
|
|
163
141
|
os.environ["FOXCLAW_TRACE_RETAIN"] = "100"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "1"
|
|
164
142
|
for i in range(140):
|
|
165
143
|
store.put(_mk(f"GET /bulk{i}", 1))
|
|
@@ -167,17 +145,14 @@ def test_tracing():
|
|
|
167
145
|
os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
|
|
168
146
|
print("[13] 落库保留上限(超出删最旧,磁盘有界)OK")
|
|
169
147
|
|
|
170
|
-
# [14] 库不可用 → 优雅降级为仅内存(不能因为落库失败就把请求搞挂)
|
|
171
148
|
s2 = TraceStore(limit=2)
|
|
172
149
|
s2.attach_db("/nonexistent-dir-xyz/なし/t.db")
|
|
173
|
-
s2.put(_mk("GET /ok", 900))
|
|
150
|
+
s2.put(_mk("GET /ok", 900))
|
|
174
151
|
assert s2.query(limit=9) == [] and len(s2.list(limit=9)) == 1
|
|
175
152
|
print("[14] 库不可用 → 降级仅内存,不影响请求 OK")
|
|
176
153
|
|
|
177
|
-
# ── W3C Trace Context:零依赖跨进程传播(Foxclaw 唯一的跨进程边界=MCP 双向)──
|
|
178
154
|
from foxclaw.core.tracing import parse_traceparent, to_traceparent, current_traceparent
|
|
179
155
|
|
|
180
|
-
# [15] traceparent 解析:合法接受,非法/全零一律拒(不信外部输入)
|
|
181
156
|
good = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
|
182
157
|
assert parse_traceparent(good) == "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
183
158
|
assert parse_traceparent("00-" + "0" * 32 + "-00f067aa0ba902b7-01") is None, "全零 trace-id 非法"
|
|
@@ -185,25 +160,21 @@ def test_tracing():
|
|
|
185
160
|
assert parse_traceparent(bad_tp) is None, bad_tp
|
|
186
161
|
print("[15] traceparent 解析(合法接受/非法+全零拒绝)OK")
|
|
187
162
|
|
|
188
|
-
# [16] trace_id 符合 W3C 规格(32 hex)→ 出站 traceparent 可被对端接受
|
|
189
163
|
t16 = Trace("w3c")
|
|
190
164
|
assert len(t16.trace_id) == 32 and all(c in "0123456789abcdef" for c in t16.trace_id), t16.trace_id
|
|
191
165
|
tp = to_traceparent(t16.trace_id)
|
|
192
166
|
assert parse_traceparent(tp) == t16.trace_id, "自己生成的应能被自己解析(自洽)"
|
|
193
167
|
print("[16] trace_id 32hex 合规 + 出站 traceparent 自洽 OK")
|
|
194
168
|
|
|
195
|
-
# [17] 入站接链:带 traceparent 的请求应**复用**其 trace_id(两边链路对齐)
|
|
196
169
|
with TestClient(app) as c:
|
|
197
170
|
r = c.post("/api/task", json={"task": "接链自测", "agent_id": "supervisor"},
|
|
198
171
|
headers={"traceparent": good})
|
|
199
172
|
assert r.headers.get("X-Trace-Id") == "4bf92f3577b34da6a3ce929d0e0e4736", r.headers.get("X-Trace-Id")
|
|
200
|
-
# 非法 traceparent → 不采信,自开一条
|
|
201
173
|
r2 = c.post("/api/task", json={"task": "非法头", "agent_id": "supervisor"},
|
|
202
174
|
headers={"traceparent": "garbage"})
|
|
203
175
|
assert r2.headers.get("X-Trace-Id") and r2.headers["X-Trace-Id"] != "garbage"
|
|
204
176
|
print("[17] 入站接链(合法 traceparent 复用其 trace_id;非法则自开)OK")
|
|
205
177
|
|
|
206
|
-
# [18] 出站:current_traceparent 在 trace 内可用、trace 外为 None(MCP 调用据此带头)
|
|
207
178
|
clear_trace()
|
|
208
179
|
assert current_traceparent() is None
|
|
209
180
|
t18 = start_trace("out")
|
|
@@ -211,17 +182,14 @@ def test_tracing():
|
|
|
211
182
|
clear_trace()
|
|
212
183
|
print("[18] 出站 current_traceparent(trace 内可用/trace 外 None)OK")
|
|
213
184
|
|
|
214
|
-
# ── OTLP/HTTP-JSON 导出(可选、零依赖、绝不阻塞请求)───────────────
|
|
215
185
|
from foxclaw.core.tracing import OTLP, _trace_to_otlp
|
|
216
186
|
|
|
217
|
-
# [19] 默认关:未配 endpoint → 完全不启用(零成本)
|
|
218
187
|
os.environ.pop("FOXCLAW_OTLP_ENDPOINT", None)
|
|
219
188
|
assert OTLP.stats()["enabled"] is False
|
|
220
|
-
OTLP.submit(_mk("GET /x", 5))
|
|
189
|
+
OTLP.submit(_mk("GET /x", 5))
|
|
221
190
|
print("[19] 未配 endpoint → OTLP 完全不启用(零成本)OK")
|
|
222
191
|
|
|
223
|
-
|
|
224
|
-
t20 = start_trace("GET /demo") # 建 trace 并绑到当前上下文
|
|
192
|
+
t20 = start_trace("GET /demo")
|
|
225
193
|
with span("child.ok"): pass
|
|
226
194
|
try:
|
|
227
195
|
with span("child.bad"):
|
|
@@ -244,7 +212,6 @@ def test_tracing():
|
|
|
244
212
|
assert svc["key"] == "service.name" and svc["value"]["stringValue"] == "foxclaw"
|
|
245
213
|
print(f"[20] OTLP/JSON 结构(根+{len(kids)} 子 span·parentSpanId·32hex traceId·状态码)OK")
|
|
246
214
|
|
|
247
|
-
# [21] 端到端:起一个本地 collector,确认真的 POST /v1/traces 且 body 可解析
|
|
248
215
|
import http.server, socketserver, threading as _th2
|
|
249
216
|
received = []
|
|
250
217
|
class _H(http.server.BaseHTTPRequestHandler):
|
|
@@ -259,7 +226,7 @@ def test_tracing():
|
|
|
259
226
|
os.environ["FOXCLAW_OTLP_ENDPOINT"] = f"http://127.0.0.1:{port}"
|
|
260
227
|
t21 = _mk("GET /exported", 12)
|
|
261
228
|
OTLP.submit(t21)
|
|
262
|
-
for _ in range(50):
|
|
229
|
+
for _ in range(50):
|
|
263
230
|
if received: break
|
|
264
231
|
_t.sleep(0.1)
|
|
265
232
|
srv.shutdown()
|
|
@@ -270,8 +237,7 @@ def test_tracing():
|
|
|
270
237
|
assert got["traceId"] == t21.trace_id and got["name"] == "GET /exported", got
|
|
271
238
|
print("[21] 端到端导出(本地 collector 收到 POST /v1/traces,traceId 对得上)OK")
|
|
272
239
|
|
|
273
|
-
|
|
274
|
-
os.environ["FOXCLAW_OTLP_ENDPOINT"] = "http://127.0.0.1:1" # 打不通 → worker 卡住,队列会满
|
|
240
|
+
os.environ["FOXCLAW_OTLP_ENDPOINT"] = "http://127.0.0.1:1"
|
|
275
241
|
from foxclaw.core.tracing import _OtlpExporter
|
|
276
242
|
ex = _OtlpExporter(maxsize=2)
|
|
277
243
|
before = ex.dropped
|
|
@@ -72,13 +72,12 @@ def test_pending_bounded_no_leak():
|
|
|
72
72
|
print("\n【3b】深审回归:持续结晶下 _pending 不无界增长(prune 已完成 Future)")
|
|
73
73
|
hkc = _HKC()
|
|
74
74
|
tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=True)
|
|
75
|
-
for i in range(200):
|
|
75
|
+
for i in range(200):
|
|
76
76
|
tc.on_truth(_msg({"truth": {"fact": f"事实{i}"}, "agent_id": "office"}))
|
|
77
|
-
tc.drain()
|
|
78
|
-
# 再来一发触发 prune,此时之前 200 个都已 done → 被清掉
|
|
77
|
+
tc.drain()
|
|
79
78
|
tc.on_truth(_msg({"truth": {"fact": "末尾"}, "agent_id": "office"}))
|
|
80
79
|
import time as _t
|
|
81
|
-
for _ in range(50):
|
|
80
|
+
for _ in range(50):
|
|
82
81
|
if all(f.done() for f in tc._pending):
|
|
83
82
|
break
|
|
84
83
|
_t.sleep(0.02)
|
|
@@ -95,9 +94,8 @@ def test_integration_wiring():
|
|
|
95
94
|
from foxclaw import Foxclaw
|
|
96
95
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
97
96
|
fake = _HKC()
|
|
98
|
-
os_.truth_crystallizer._hkc_provider = lambda: fake
|
|
97
|
+
os_.truth_crystallizer._hkc_provider = lambda: fake
|
|
99
98
|
os_.truth_crystallizer.enabled = True
|
|
100
|
-
# 真值链末端事件 → 结晶器应被 Foxclaw 订阅到并触发
|
|
101
99
|
os_.system_bus.publish("truth.appended",
|
|
102
100
|
{"truth": {"fact": "测试真值事实"}, "agent_id": "finance"})
|
|
103
101
|
os_.truth_crystallizer.drain()
|
|
@@ -15,7 +15,7 @@ import os, sys, asyncio, tempfile, shutil
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
17
17
|
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
18
|
-
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
18
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
19
19
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
20
20
|
|
|
21
21
|
|
|
@@ -30,13 +30,11 @@ def main():
|
|
|
30
30
|
seen = []
|
|
31
31
|
os_.event_spine.project(lambda e: seen.append(e.topic))
|
|
32
32
|
|
|
33
|
-
# 1. _no_emit 守卫:子 Agent 不自发
|
|
34
33
|
print("\n【1】_no_emit 守卫:分发时子 Agent 不自发 execution.completed")
|
|
35
34
|
asyncio.run(os_.finance.run_task("守卫测试", {"_no_emit": True}))
|
|
36
35
|
assert "execution.completed" not in seen, "带 _no_emit 时不应发 execution.completed"
|
|
37
36
|
print(" ✓ context['_no_emit']=True → 子 Agent 未自发(留给主管统一发)")
|
|
38
37
|
|
|
39
|
-
# 2. 全路径触发:直连任务发恰好一条
|
|
40
38
|
print("\n【2】全路径触发:直连领域 Agent 任务发恰好一条 execution.completed")
|
|
41
39
|
before = seen.count("execution.completed")
|
|
42
40
|
asyncio.run(os_.finance.run_task("分析我的月度开支"))
|
|
@@ -44,7 +42,6 @@ def main():
|
|
|
44
42
|
assert emitted == 1, f"直连任务应发恰好 1 条,实得 {emitted}"
|
|
45
43
|
print(f" ✓ 直连 finance.run_task → execution.completed ×{emitted}")
|
|
46
44
|
|
|
47
|
-
# 3. 共享真值记录:别的 Agent 能召回
|
|
48
45
|
print("\n【3】共享真值记录:直连任务经 TruthPipeline 产生 shared 记录,跨 Agent 可见")
|
|
49
46
|
shared = os_.hmr.recall(query="分析我的月度开支 处理", agent_id="office", top_k=10)
|
|
50
47
|
shared_hits = [m for m in shared if m.get("shared")]
|
|
@@ -52,7 +49,6 @@ def main():
|
|
|
52
49
|
print(f" ✓ office 召回到 {len(shared_hits)} 条共享真值记录(来自 finance 的直连任务)")
|
|
53
50
|
print(f" 样例: {shared_hits[0].get('content','')[:50]!r}")
|
|
54
51
|
|
|
55
|
-
# 4. 被拦截的直连任务:发 blocked=True、不落共享真值记录、CognitiveState 不崩
|
|
56
52
|
print("\n【4】被拦截的直连任务:blocked=True + 不落共享记录 + 观测不崩")
|
|
57
53
|
import types
|
|
58
54
|
from foxclaw.agents.base import HarnessBlockedError
|
|
@@ -68,11 +64,10 @@ def main():
|
|
|
68
64
|
b_emitted = [x for x in blocked_flags if x is True]
|
|
69
65
|
assert r.get("error") == "harness_blocked"
|
|
70
66
|
assert len(b_emitted) - b_before == 1, "被拦截直连应发恰好一条 blocked=True"
|
|
71
|
-
# 精确判定:blocked 任务本身没产生真值记录(按内容过滤;语义召回可能带回 check2 的旧记录)
|
|
72
67
|
own = [m for m in os_.hmr.recall(query="危险越界任务 处理", agent_id="office", top_k=10)
|
|
73
68
|
if m.get("shared") and "危险越界" in m.get("content", "")]
|
|
74
69
|
assert len(own) == 0, "被拦截任务不应产生自己的共享真值记录(TruthPipeline 跳过 blocked)"
|
|
75
|
-
snap = os_.cognitive_state.snapshot()
|
|
70
|
+
snap = os_.cognitive_state.snapshot()
|
|
76
71
|
assert "active_agent" in snap
|
|
77
72
|
print(f" ✓ blocked=True ×1、无共享记录、CognitiveState 快照正常(focus={snap.get('semantic_focus')})")
|
|
78
73
|
|