foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -7,7 +7,6 @@ import os, sys, tempfile, shutil
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
9
9
|
|
|
10
|
-
# ── 环境隔离:必须在 import foxclaw 之前(import 会读这些键初始化) ─────────────
|
|
11
10
|
for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
|
|
12
11
|
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
13
12
|
os.environ.pop(_k, None)
|
|
@@ -15,10 +14,10 @@ os.environ["HKC_EMBEDDING"] = "stub"
|
|
|
15
14
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
16
15
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_att_")
|
|
17
16
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
18
|
-
os.environ["FOXCLAW_PROVIDER"] = ""
|
|
17
|
+
os.environ["FOXCLAW_PROVIDER"] = ""
|
|
19
18
|
os.environ["FOXCLAW_MODEL"] = ""
|
|
20
19
|
os.environ["FOXCLAW_MCP_SERVER"] = "0"
|
|
21
|
-
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
20
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
22
21
|
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
23
22
|
|
|
24
23
|
import pytest
|
|
@@ -40,31 +39,25 @@ def store():
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def test_sanitize_attachments():
|
|
43
|
-
# 数量上限
|
|
44
42
|
many = [{"name": f"f{i}.txt", "text": "t"} for i in range(_MAX_ATTACHMENTS + 4)]
|
|
45
43
|
assert len(_sanitize_attachments(many)) == _MAX_ATTACHMENTS
|
|
46
44
|
|
|
47
|
-
# 单文件正文截断到上限
|
|
48
45
|
one = _sanitize_attachments([{"name": "a.txt", "text": "x" * (_ATTACH_TEXT_MAX + 100)}])
|
|
49
46
|
assert len(one[0]["text"]) == _ATTACH_TEXT_MAX
|
|
50
47
|
|
|
51
|
-
# name 截断到 200
|
|
52
48
|
assert _sanitize_attachments([{"name": "a" * 300, "text": "x"}])[0]["name"] == "a" * 200
|
|
53
49
|
|
|
54
|
-
# 累计正文封顶:单文件先截到 20000,4×20000 累计 60000 封顶 → 第 4 个正文被清空(名字仍在)
|
|
55
50
|
big = "x" * (_ATTACH_TEXT_MAX + 1000)
|
|
56
51
|
four = _sanitize_attachments([{"name": f"f{i}.txt", "text": big} for i in range(4)])
|
|
57
52
|
assert len(four) == 4
|
|
58
53
|
assert sum(len(a["text"]) for a in four) == _ATTACH_TEXT_TOTAL
|
|
59
54
|
assert four[3]["text"] == ""
|
|
60
55
|
|
|
61
|
-
# 非列表 / 非 dict / 空 name+空 text → 全部剔除
|
|
62
56
|
assert _sanitize_attachments("notalist") == []
|
|
63
57
|
assert _sanitize_attachments([1, "x"]) == []
|
|
64
58
|
assert _sanitize_attachments([{"name": "", "text": ""}]) == []
|
|
65
59
|
assert _sanitize_attachments(None) == []
|
|
66
60
|
|
|
67
|
-
# 字段类型清洗:size 非数字 → 0;mime/url 转 str
|
|
68
61
|
out = _sanitize_attachments([{"name": "a", "size": "big", "mime": None, "url": None, "text": "t"}])
|
|
69
62
|
assert out[0]["size"] == 0 and out[0]["mime"] == "" and out[0]["url"] == ""
|
|
70
63
|
print("护栏(数量/单文件/累计/类型清洗/边界)OK")
|
|
@@ -78,10 +71,10 @@ def test_attachments_context():
|
|
|
78
71
|
ctx = CognitiveAgent._attachments_context(atts)
|
|
79
72
|
assert "【附件「a.txt」内容如下】" in ctx and "hello world" in ctx
|
|
80
73
|
assert "【附件「b.txt」" not in ctx
|
|
81
|
-
assert "b.pdf" in ctx and "无法提取" in ctx
|
|
74
|
+
assert "b.pdf" in ctx and "无法提取" in ctx
|
|
82
75
|
assert CognitiveAgent._attachments_context([]) == ""
|
|
83
76
|
assert CognitiveAgent._attachments_context(None) == ""
|
|
84
|
-
assert CognitiveAgent._attachments_context([1, "x"]) == ""
|
|
77
|
+
assert CognitiveAgent._attachments_context([1, "x"]) == ""
|
|
85
78
|
print("LLM 上下文注入(正文/降级/空)OK")
|
|
86
79
|
|
|
87
80
|
|
|
@@ -98,7 +91,6 @@ def test_sessions_append_attachments(store):
|
|
|
98
91
|
assert len(m["attachments"]) == 2
|
|
99
92
|
assert m["attachments"][0] == {"name": "a.txt", "size": 11, "mime": "text/plain", "url": "/api/attachments/abc"}
|
|
100
93
|
assert "text" not in m["attachments"][0], "text 不得落盘(会话文件不膨胀)"
|
|
101
|
-
# 不带附件 → 无 attachments 字段(向后兼容)
|
|
102
94
|
assert store.append(s["id"], "assistant", "好")
|
|
103
95
|
assert "attachments" not in store.get(s["id"])["messages"][1]
|
|
104
96
|
print("会话落盘(轻字段/不存 text/向后兼容)OK")
|
|
@@ -106,42 +98,35 @@ def test_sessions_append_attachments(store):
|
|
|
106
98
|
|
|
107
99
|
def test_upload_and_download():
|
|
108
100
|
with TestClient(app) as c:
|
|
109
|
-
# 上传文本文件 → 提取正文
|
|
110
101
|
r = c.post("/api/upload", files={"file": ("笔记.txt", "今天学了三件事".encode(), "text/plain")})
|
|
111
102
|
assert r.status_code == 200, r.text
|
|
112
103
|
a = r.json()
|
|
113
104
|
assert a["name"] == "笔记.txt" and a["text"] == "今天学了三件事"
|
|
114
105
|
assert a["url"].startswith("/api/attachments/") and a["id"] and a["size"] > 0
|
|
115
106
|
|
|
116
|
-
# 下载 → 同一内容
|
|
117
107
|
r2 = c.get(a["url"])
|
|
118
108
|
assert r2.status_code == 200
|
|
119
109
|
assert r2.content.decode("utf-8") == "今天学了三件事"
|
|
120
110
|
|
|
121
|
-
# 二进制(不可提取)→ text 为空,降级
|
|
122
111
|
r3 = c.post("/api/upload", files={"file": ("img.bin", b"\x00\x01\x02", "application/octet-stream")})
|
|
123
112
|
assert r3.status_code == 200 and r3.json()["text"] == ""
|
|
124
113
|
|
|
125
|
-
# 空文件 → 400;超大 → 413
|
|
126
114
|
assert c.post("/api/upload", files={"file": ("e.txt", b"", "text/plain")}).status_code == 400
|
|
127
115
|
big = b"x" * (21 * 1024 * 1024)
|
|
128
116
|
assert c.post("/api/upload", files={"file": ("big.txt", big, "text/plain")}).status_code == 413
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
assert c.get("/api/attachments
|
|
132
|
-
assert c.get("/api/attachments/
|
|
133
|
-
assert c.get("/api/attachments/deadbeef0000").status_code == 404 # 合法 id 但文件不存在
|
|
118
|
+
assert c.get("/api/attachments/../../etc/passwd").status_code == 404
|
|
119
|
+
assert c.get("/api/attachments/a.b").status_code == 400
|
|
120
|
+
assert c.get("/api/attachments/deadbeef0000").status_code == 404
|
|
134
121
|
print("上传/下载(正文提取/二进制降级/空/超大/穿越/404)OK")
|
|
135
122
|
|
|
136
123
|
|
|
137
124
|
def test_chat_stream_with_attachments():
|
|
138
125
|
import foxclaw.api as _api
|
|
139
126
|
with TestClient(app) as c:
|
|
140
|
-
# 先上传一个带正文的附件,拿到引用
|
|
141
127
|
up = c.post("/api/upload", files={"file": ("要点.txt", "预算 100 万".encode(), "text/plain")})
|
|
142
128
|
att = up.json()
|
|
143
|
-
|
|
144
|
-
sid = _api._os_instance.sessions.create()["id"]
|
|
129
|
+
sid = _api.os_instance().sessions.create()["id"]
|
|
145
130
|
payload = {
|
|
146
131
|
"task": "帮我总结要点",
|
|
147
132
|
"agent_id": "supervisor",
|
|
@@ -153,8 +138,7 @@ def test_chat_stream_with_attachments():
|
|
|
153
138
|
assert r.status_code == 200, r.text[:200]
|
|
154
139
|
assert "data:" in r.text and '"error"' not in r.text, r.text[:300]
|
|
155
140
|
|
|
156
|
-
|
|
157
|
-
d = _api._os_instance.sessions.get(sid)
|
|
141
|
+
d = _api.os_instance().sessions.get(sid)
|
|
158
142
|
assert d is not None, "会话未落盘"
|
|
159
143
|
um = d["messages"][0]
|
|
160
144
|
assert um["role"] == "user" and um["content"] == "帮我总结要点"
|
|
@@ -24,8 +24,6 @@ def _write_chain(path: Path, records) -> None:
|
|
|
24
24
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
# ── 链 ─────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
27
|
def test_chain_passes(tmp_path):
|
|
30
28
|
from foxclaw.core import audit_chain
|
|
31
29
|
|
|
@@ -42,7 +40,7 @@ def test_tampered_line_detected(tmp_path):
|
|
|
42
40
|
_write_chain(p, [{"verdict": "deny"}, {"verdict": "allow"}])
|
|
43
41
|
lines = p.read_text(encoding="utf-8").splitlines()
|
|
44
42
|
rec = json.loads(lines[0])
|
|
45
|
-
rec["verdict"] = "allow"
|
|
43
|
+
rec["verdict"] = "allow"
|
|
46
44
|
lines[0] = json.dumps(rec, ensure_ascii=False)
|
|
47
45
|
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
48
46
|
|
|
@@ -69,7 +67,7 @@ def test_resume_after_restart(tmp_path):
|
|
|
69
67
|
p = tmp_path / "governance.jsonl"
|
|
70
68
|
GovernanceAuditor(sink_path=p).record(
|
|
71
69
|
layer="skill", actor="office", action="write_note", verdict="success")
|
|
72
|
-
aud = GovernanceAuditor(sink_path=p)
|
|
70
|
+
aud = GovernanceAuditor(sink_path=p)
|
|
73
71
|
aud.record(layer="skill", actor="office", action="write_note", verdict="success")
|
|
74
72
|
|
|
75
73
|
ok, n, msg = aud.verify()
|
|
@@ -96,8 +94,6 @@ def test_tail_hash_ignores_torn_line(tmp_path):
|
|
|
96
94
|
assert audit_chain.tail_hash(p) == "h1"
|
|
97
95
|
|
|
98
96
|
|
|
99
|
-
# ── recovery 脱敏 ──────────────────────────────────────────────────
|
|
100
|
-
|
|
101
97
|
def test_recovery_step_args_and_result_redacted():
|
|
102
98
|
from types import SimpleNamespace
|
|
103
99
|
|
|
@@ -117,5 +113,4 @@ def test_recovery_step_args_and_result_redacted():
|
|
|
117
113
|
|
|
118
114
|
assert "sk-" not in json.dumps(got["args"]), got["args"]
|
|
119
115
|
assert "[已脱敏]" in got["result"], got["result"]
|
|
120
|
-
# key 必须用未脱敏的 args 算,否则同一步骤前后 key 不一致(缓存/回放对不上)
|
|
121
116
|
assert got["key"] == _step_key("http", {"api_key": key})
|
|
@@ -42,7 +42,6 @@ def test_unit():
|
|
|
42
42
|
"reason": "office 的职责域不含 ['type:decision','finance.transfer']",
|
|
43
43
|
"mode": "audit", "content_sha": "0123456789abcdef",
|
|
44
44
|
"content_preview": "财务应给办公转账1000元"}, source="office")
|
|
45
|
-
# 非本主题的事件不应落盘(subscribe 前缀精确命中)
|
|
46
45
|
spine.publish("memory.updated", {"x": 1})
|
|
47
46
|
|
|
48
47
|
check(path.exists(), "审计文件已创建(含 audit/ 目录)")
|
|
@@ -55,7 +54,6 @@ def test_unit():
|
|
|
55
54
|
check("ts" in r and "T" in r["ts"], "ts 为 ISO 时间(人类可读)")
|
|
56
55
|
check(bool(r["correlation_id"]), "correlation_id 已记录(可与脊柱 history 对账)")
|
|
57
56
|
|
|
58
|
-
# 追加语义:第二条接在后面,顺序保持
|
|
59
57
|
spine.publish("memory.shared.write", {"agent_id": "system", "type": "execution",
|
|
60
58
|
"tags": ["truth_pipeline"], "allowed": True,
|
|
61
59
|
"reason": "命中全局允许 facet", "mode": "audit",
|
|
@@ -65,12 +63,10 @@ def test_unit():
|
|
|
65
63
|
check(len(recs) == 2 and recs[1]["payload"]["agent_id"] == "system",
|
|
66
64
|
"追加写不覆盖旧行,顺序保持")
|
|
67
65
|
|
|
68
|
-
# 坏行容错:夹一行垃圾,read() 跳过而不是崩
|
|
69
66
|
with open(path, "a", encoding="utf-8") as f:
|
|
70
67
|
f.write("{这不是JSON}\n")
|
|
71
68
|
check(len(sink.read()) == 2, "坏行被跳过,读取不崩")
|
|
72
69
|
|
|
73
|
-
# 开关:环境变量置 0 时 attach 不挂
|
|
74
70
|
old = os.environ.get(ENV_SWITCH)
|
|
75
71
|
os.environ[ENV_SWITCH] = "0"
|
|
76
72
|
try:
|
|
@@ -99,7 +95,6 @@ def test_end_to_end():
|
|
|
99
95
|
audit_path = Path(tmp) / "audit" / "shared_write.jsonl"
|
|
100
96
|
check(audit_path.parent.exists(), "audit/ 目录已建在 data_dir 下")
|
|
101
97
|
|
|
102
|
-
# 一条合法 + 一条越界(audit 模式不拦截,只记录)——真实闸门口径
|
|
103
98
|
osi.hmr.ingest("用户偏好:喜欢表格", type="concept", agent_id="finance",
|
|
104
99
|
tags=["preference"], shared=True)
|
|
105
100
|
osi.hmr.ingest("财务应给办公转账1000元", type="decision", agent_id="office",
|
|
@@ -112,7 +107,6 @@ def test_end_to_end():
|
|
|
112
107
|
denied = [r for r in recs if not r["payload"]["allowed"]]
|
|
113
108
|
check(bool(denied) and "office" in denied[0]["payload"]["reason"] or bool(denied),
|
|
114
109
|
"越界那条可从文件直接判读")
|
|
115
|
-
# 与脊柱 history 口径一致:文件条数 == 脊柱里该主题条数
|
|
116
110
|
spine_n = len(osi.event_spine.history("memory.shared.write", 100))
|
|
117
111
|
check(spine_n == len(recs), f"文件条数与脊柱 history 一致({len(recs)}=={spine_n})")
|
|
118
112
|
finally:
|
|
@@ -91,10 +91,8 @@ async def test_service_wrapper():
|
|
|
91
91
|
print("\n【3】BrowserService async 包装(注入假 session,经专用工作线程)")
|
|
92
92
|
svc = BrowserService()
|
|
93
93
|
fake = FakeSession()
|
|
94
|
-
# 绕过 playwright:预置会话,则真实 _ensure_session_sync 因 _session 非 None 直接返回它,
|
|
95
|
-
# 不构造 SessionManager/不 launch chromium。强制 available=True 走真实工作线程路径。
|
|
96
94
|
svc._session = fake
|
|
97
|
-
svc.available = lambda: True
|
|
95
|
+
svc.available = lambda: True
|
|
98
96
|
|
|
99
97
|
p = await svc.perceive()
|
|
100
98
|
assert p["ok"] and p["semantic_state"]["title"] == "P1"
|
|
@@ -104,16 +102,14 @@ async def test_service_wrapper():
|
|
|
104
102
|
assert r["ok"] and r["verified"]["success"]
|
|
105
103
|
print(" ✓ act(navigate) 返回校验后结果 verified.success=True")
|
|
106
104
|
|
|
107
|
-
bad = await svc.act({"target": "3"})
|
|
105
|
+
bad = await svc.act({"target": "3"})
|
|
108
106
|
assert bad["ok"] is False and "非法动作" in bad["error"]
|
|
109
107
|
print(" ✓ 非法动作(缺 type)被结构化拒绝")
|
|
110
108
|
|
|
111
109
|
await svc.close()
|
|
112
110
|
assert fake.closed, "close() 必须在工作线程上释放会话"
|
|
113
111
|
print(" ✓ close() 释放了会话")
|
|
114
|
-
# 幂等:再次 close 不报错
|
|
115
112
|
await svc.close()
|
|
116
|
-
# 关停后调用被拒(executor 已关)
|
|
117
113
|
after_close = await svc.perceive()
|
|
118
114
|
assert after_close["ok"] is False
|
|
119
115
|
print(" ✓ close() 幂等 + 关停后调用被安全拒绝")
|
|
@@ -10,8 +10,6 @@ from foxclaw.browser.action import url_policy
|
|
|
10
10
|
from foxclaw.browser.driver.playwright_driver import PlaywrightDriver
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
# ── 判定层 ────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
13
|
@pytest.mark.parametrize("url", [
|
|
16
14
|
"data:image/png;base64,iVBORw0KGgo=",
|
|
17
15
|
"blob:https://example.com/2f8c-4a",
|
|
@@ -112,8 +110,6 @@ def test_short_ipv6_with_scope_is_not_mistaken(monkeypatch):
|
|
|
112
110
|
url_policy._resolve_cache.clear()
|
|
113
111
|
|
|
114
112
|
|
|
115
|
-
# ── 路由回调 ──────────────────────────────────────────────────
|
|
116
|
-
|
|
117
113
|
class _FakeRoute:
|
|
118
114
|
def __init__(self, url):
|
|
119
115
|
self.request = type("Req", (), {"url": url})()
|
|
@@ -43,10 +43,8 @@ def test_cycles_broken_structural():
|
|
|
43
43
|
try:
|
|
44
44
|
from foxclaw import Foxclaw
|
|
45
45
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
46
|
-
# 环1:Agent 不再直接持 search_runtime
|
|
47
46
|
assert not hasattr(os_.finance, "_search_runtime"), "Agent 不应有 _search_runtime 直接引用"
|
|
48
47
|
assert os_.finance._caps is os_.capabilities, "Agent 改持能力中枢"
|
|
49
|
-
# 环2:Orchestrator 不再把 agents 存为实例字段(改为经中枢解析的 property)
|
|
50
48
|
assert "agents" not in os_.orchestrator.__dict__, "orchestrator 不应把 agents 存为实例字段"
|
|
51
49
|
assert os_.orchestrator._caps is os_.capabilities
|
|
52
50
|
assert os_.orchestrator.agents is os_._agents, "orchestrator.agents 经中枢解析"
|
|
@@ -63,17 +61,14 @@ def test_behavior_preserved():
|
|
|
63
61
|
try:
|
|
64
62
|
from foxclaw import Foxclaw
|
|
65
63
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
66
|
-
# 跨 Agent 编排(orchestrator 经中枢解析 finance)
|
|
67
64
|
r = os_.supervisor.cross_request(
|
|
68
65
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
69
66
|
payload={"amount": 150, "category": "travel", "entity": "高铁", "account": "default"},
|
|
70
67
|
reason="出差")
|
|
71
68
|
assert r["status"] in ("approved", "confirm_required", "rejected"), f"裁决异常 {r}"
|
|
72
69
|
print(f" ✓ cross_request 经中枢解析 agents → 状态 {r['status']}")
|
|
73
|
-
# Agent 反向调用内部搜索(经中枢解析 search_runtime)
|
|
74
70
|
res = os_.finance.search_internal("测试查询")
|
|
75
71
|
assert res is not None, "search_internal 应经中枢解析到 search_runtime"
|
|
76
|
-
# 中枢缓存:两次解析同一实例
|
|
77
72
|
assert os_.capabilities.get("search_runtime") is os_.search_runtime
|
|
78
73
|
print(" ✓ search_internal 经中枢解析 search_runtime(缓存一致)")
|
|
79
74
|
asyncio.run(os_.aclose())
|
|
@@ -101,7 +96,6 @@ def test_lazy_build_once_concurrent():
|
|
|
101
96
|
for t in ts: t.join()
|
|
102
97
|
assert calls["n"] == 1, f"并发下惰性应只构造 1 次,实得 {calls['n']}"
|
|
103
98
|
assert len({id(r) for r in results}) == 1, "12 线程应拿到同一实例"
|
|
104
|
-
# 不同能力不互相阻塞(构造锁 per-name;此处只验证正确性:各自构造一次)
|
|
105
99
|
reg.register_lazy("y", lambda: object())
|
|
106
100
|
assert reg.get("y") is reg.get("y")
|
|
107
101
|
print(" ✓ 12 线程并发 get 同一惰性 → 只构造 1 次、同一实例;不同能力独立")
|
|
@@ -23,8 +23,6 @@ def _headers(secret: str = SECRET, body: bytes = b'{"a":1}', ts: float = 1_700_0
|
|
|
23
23
|
return signer.headers(body, nonce="n0nce", now=ts)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
# ── canonical / 签名 ────────────────────────────────────────
|
|
27
|
-
|
|
28
26
|
def test_canonical_format_is_locked() -> None:
|
|
29
27
|
"""canonical 是两端的协议:字段顺序/分隔符一改,对方全部验签失败。"""
|
|
30
28
|
line = S.canonical("1700000000", "abc", b"body").decode()
|
|
@@ -63,7 +61,6 @@ def test_replay_guard_rejects_second_use() -> None:
|
|
|
63
61
|
guard = S.ReplayGuard(ttl=300)
|
|
64
62
|
assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
|
|
65
63
|
assert S.verify(h, body, SECRET, replay=guard, now=1_700_000_000) == (False, "replay")
|
|
66
|
-
# 换一个 nonce(正常新请求)→ 通过
|
|
67
64
|
h2 = S.OutboundSigner(SECRET).headers(body, nonce="other", now=1_700_000_000)
|
|
68
65
|
assert S.verify(h2, body, SECRET, replay=guard, now=1_700_000_000) == (True, "ok")
|
|
69
66
|
|
|
@@ -82,12 +79,10 @@ def test_verify_reason_codes() -> None:
|
|
|
82
79
|
|
|
83
80
|
def test_header_lookup_is_case_insensitive() -> None:
|
|
84
81
|
body = b"{}"
|
|
85
|
-
h = {k.lower(): v for k, v in _headers(body=body).items()}
|
|
82
|
+
h = {k.lower(): v for k, v in _headers(body=body).items()}
|
|
86
83
|
assert S.verify(h, body, SECRET, now=1_700_000_000) == (True, "ok")
|
|
87
84
|
|
|
88
85
|
|
|
89
|
-
# ── signer ─────────────────────────────────────────────────
|
|
90
|
-
|
|
91
86
|
def test_signer_optional_key_id_and_disabled_state() -> None:
|
|
92
87
|
h = S.OutboundSigner(SECRET, key_id="k9").headers(b"{}")
|
|
93
88
|
assert h[S.KEY_ID_HEADER] == "k9"
|
|
@@ -105,8 +100,6 @@ def test_from_env_opt_in() -> None:
|
|
|
105
100
|
assert S.from_env({S.ENV_SECRET: " "}) is None
|
|
106
101
|
|
|
107
102
|
|
|
108
|
-
# ── 桥接层 ─────────────────────────────────────────────────
|
|
109
|
-
|
|
110
103
|
class _Resp:
|
|
111
104
|
status_code = 200
|
|
112
105
|
text = ""
|
|
@@ -155,7 +148,7 @@ def test_bridge_signs_the_bytes_it_sends(fake_http) -> None:
|
|
|
155
148
|
assert isinstance(body, bytes) and json.loads(body)["text"] == "hi"
|
|
156
149
|
assert cap["headers"]["Content-Type"] == "application/json"
|
|
157
150
|
assert cap["headers"]["Authorization"] == "Bearer t"
|
|
158
|
-
ok, why = S.verify(cap["headers"], body, SECRET)
|
|
151
|
+
ok, why = S.verify(cap["headers"], body, SECRET)
|
|
159
152
|
assert (ok, why) == (True, "ok")
|
|
160
153
|
|
|
161
154
|
|
|
@@ -23,7 +23,6 @@ def check(cond, msg):
|
|
|
23
23
|
def test_abstraction():
|
|
24
24
|
print("\n【1】渠道抽象层")
|
|
25
25
|
from foxclaw.channels import ChannelAdapter, InboundMessage, SendResult, ChannelRegistry, OutboundMessage
|
|
26
|
-
# handle_raw 异常隔离:on_message 抛异常不冒泡
|
|
27
26
|
class A(ChannelAdapter):
|
|
28
27
|
CHANNEL = "t"
|
|
29
28
|
def normalize(self, raw):
|
|
@@ -38,7 +37,6 @@ def test_abstraction():
|
|
|
38
37
|
for txt in ["a", "bad", "c"]:
|
|
39
38
|
ad.handle_raw({"text": txt})
|
|
40
39
|
check(processed == ["a", "c"], "handle_raw 异常隔离(单条失败不影响其他)")
|
|
41
|
-
# 注册表
|
|
42
40
|
reg = ChannelRegistry(); reg.register(ad)
|
|
43
41
|
check(reg.get("t") is ad, "渠道注册表")
|
|
44
42
|
check(not reg.send("nope", OutboundMessage("c", "x")).ok, "未注册渠道发送失败")
|
|
@@ -69,14 +67,12 @@ def test_weixin():
|
|
|
69
67
|
check(h["Authorization"].startswith("Bearer "), "Bearer token")
|
|
70
68
|
check(int(base64.b64decode(h["X-WECHAT-UIN"]).decode()) >= 0, "X-WECHAT-UIN 格式")
|
|
71
69
|
check(wx._build_client_version("2.4.3") == 132099, "ClientVersion 计算")
|
|
72
|
-
# 标准化 + context_token 记忆
|
|
73
70
|
msg = {"message_id": 1, "from_user_id": "u1", "session_id": "s",
|
|
74
71
|
"message_type": MSG_FROM_USER, "context_token": "ctx1",
|
|
75
72
|
"item_list": [{"type": ITEM_TEXT, "text_item": {"text": "你好"}}]}
|
|
76
73
|
inb = wx._normalize_msg(msg)
|
|
77
74
|
check(inb.text == "你好", "消息标准化")
|
|
78
75
|
check(wx._context_tokens.get("u1") == "ctx1", "记住 context_token")
|
|
79
|
-
# bot 回显忽略
|
|
80
76
|
check(wx._normalize_msg({"message_type": 2, "item_list": []}) is None, "bot 回显忽略")
|
|
81
77
|
|
|
82
78
|
|
|
@@ -84,10 +80,8 @@ def test_feishu():
|
|
|
84
80
|
print("\n【4】飞书")
|
|
85
81
|
from foxclaw.channels.feishu import FeishuAdapter
|
|
86
82
|
fs = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="my_secret_key_123")
|
|
87
|
-
# URL 验证
|
|
88
83
|
r = fs.handle_event({"type": "url_verification", "challenge": "c1"})
|
|
89
84
|
check(r.get("challenge") == "c1", "URL 验证 challenge")
|
|
90
|
-
# 加密解密端到端
|
|
91
85
|
def encrypt(plaintext, key_str):
|
|
92
86
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
93
87
|
key = hashlib.sha256(key_str.encode()).digest()
|
|
@@ -100,10 +94,8 @@ def test_feishu():
|
|
|
100
94
|
original = {"type": "event", "x": "测试"}
|
|
101
95
|
dec = fs.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123"))
|
|
102
96
|
check(dec == original, "AES 加密解密端到端")
|
|
103
|
-
# 错误密钥明确报错
|
|
104
97
|
fs2 = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="wrong")
|
|
105
98
|
check(fs2.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123")) is None, "错误密钥解密失败")
|
|
106
|
-
# 消息标准化
|
|
107
99
|
event = {"header": {"event_type": "im.message.receive_v1"},
|
|
108
100
|
"event": {"message": {"message_id": "om1", "chat_id": "oc1", "chat_type": "p2p",
|
|
109
101
|
"content": json.dumps({"text": "查日程"}), "create_time": "1718900000000"},
|
|
@@ -141,7 +133,7 @@ def test_closed_loop():
|
|
|
141
133
|
|
|
142
134
|
import tempfile, shutil
|
|
143
135
|
data_dir = tempfile.mkdtemp(prefix="foxclaw_ch_test_")
|
|
144
|
-
shutil.rmtree(data_dir, ignore_errors=True)
|
|
136
|
+
shutil.rmtree(data_dir, ignore_errors=True)
|
|
145
137
|
os_ = Foxclaw(data_dir=data_dir, shadow_rounds=50)
|
|
146
138
|
disp = ChannelDispatcher(os_.supervisor)
|
|
147
139
|
mock = Mock(); disp.register_channel(mock)
|
|
@@ -151,12 +143,10 @@ def test_closed_loop():
|
|
|
151
143
|
sender_id="u1", conversation_id="c1")
|
|
152
144
|
r1 = await disp.handle(msg)
|
|
153
145
|
check(r1["replied"], "闭环:收→主管→回")
|
|
154
|
-
# 幂等:同消息重发只处理一次
|
|
155
146
|
r2 = await disp.handle(msg)
|
|
156
147
|
check(r2.get("idempotent_skip"), "重发消息被幂等跳过")
|
|
157
148
|
check(len(mock.sent) == 1, "重发只回复一次")
|
|
158
149
|
asyncio.run(go())
|
|
159
|
-
# 清理
|
|
160
150
|
import shutil
|
|
161
151
|
shutil.rmtree(data_dir, ignore_errors=True)
|
|
162
152
|
|
|
@@ -9,7 +9,6 @@ def test_config_layering():
|
|
|
9
9
|
|
|
10
10
|
from foxclaw.core.config import LayeredConfig, load_policy_layer
|
|
11
11
|
|
|
12
|
-
# 1) LayeredConfig:标量高层覆盖;集合三层并集(只增不减);policy_locked
|
|
13
12
|
cfg = LayeredConfig(policy={"model": "P", "deny": ["a"]},
|
|
14
13
|
user={"model": "U", "deny": ["b"]},
|
|
15
14
|
runtime={"model": "R", "deny": ["c"], "x": 1})
|
|
@@ -19,7 +18,6 @@ def test_config_layering():
|
|
|
19
18
|
assert cfg.policy_locked("deny") == {"a"}, "policy 层锁定项"
|
|
20
19
|
print("[1] LayeredConfig 标量覆盖 + 集合并集 + policy_locked OK")
|
|
21
20
|
|
|
22
|
-
# 2) load_policy_layer:env + policy.json 合并;默认空
|
|
23
21
|
d = tempfile.mkdtemp(prefix="e1_")
|
|
24
22
|
try:
|
|
25
23
|
os.environ["FOXCLAW_POLICY_DENY"] = "gmail_send, danger_tool"
|
|
@@ -28,7 +26,6 @@ def test_config_layering():
|
|
|
28
26
|
assert set(pl["deny"]) == {"gmail_send", "danger_tool", "file_delete"}, pl
|
|
29
27
|
print(f"[2] load_policy_layer(env + policy.json) = {pl['deny']} OK")
|
|
30
28
|
|
|
31
|
-
# 3) SkillPolicy:policy 层 deny 生效 + undeny 撤不掉 + 从盘 user deny 生效(引用未断) + user 可撤
|
|
32
29
|
(Path(d) / "skill_policy.json").write_text(
|
|
33
30
|
json.dumps({"deny": ["user_denied"], "allow": []}), encoding="utf-8")
|
|
34
31
|
from foxclaw.skills.policy import SkillPolicy
|
|
@@ -45,7 +42,6 @@ def test_config_layering():
|
|
|
45
42
|
os.environ.pop("FOXCLAW_POLICY_DENY", None)
|
|
46
43
|
shutil.rmtree(d, ignore_errors=True)
|
|
47
44
|
|
|
48
|
-
# 4) behavior-preserving:无 policy 层(无 env、无 policy.json)→ SkillPolicy 行为 = 现状
|
|
49
45
|
d2 = tempfile.mkdtemp(prefix="e1b_")
|
|
50
46
|
try:
|
|
51
47
|
from foxclaw.skills.policy import SkillPolicy
|
|
@@ -25,15 +25,12 @@ from foxclaw.skills import pipeline as sk_pipeline
|
|
|
25
25
|
SESSION = "sess-1"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
# ── broker 单元 ────────────────────────────────────────────
|
|
29
|
-
|
|
30
28
|
def test_issue_consume_single_use() -> None:
|
|
31
29
|
b = ConfirmationBroker(ttl=300, clock=lambda: 1000.0)
|
|
32
30
|
ch = b.issue(SESSION, "skill:mail_send", {"to": "a@x"})
|
|
33
31
|
assert ch["nonce"] and ch["expires_at"] == 1300.0
|
|
34
32
|
assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
|
|
35
33
|
now=1000.0) == (True, "ok")
|
|
36
|
-
# 第二次:已消费 → 拒绝(单次使用)
|
|
37
34
|
assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
|
|
38
35
|
now=1000.0) == (False, "unknown_nonce")
|
|
39
36
|
|
|
@@ -48,12 +45,10 @@ def test_expired_nonce_rejected() -> None:
|
|
|
48
45
|
def test_args_and_action_drift_rejected() -> None:
|
|
49
46
|
b = ConfirmationBroker(clock=lambda: 1000.0)
|
|
50
47
|
ch = b.issue(SESSION, "skill:mail_send", {"to": "a@x"})
|
|
51
|
-
# 确认"发给 A"却执行"发给 B" → 指纹不匹配
|
|
52
48
|
assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "b@x"},
|
|
53
49
|
now=1000.0) == (False, "args_mismatch")
|
|
54
50
|
assert b.consume(SESSION, ch["nonce"], "skill:pay", {"to": "a@x"},
|
|
55
51
|
now=1000.0) == (False, "action_mismatch")
|
|
56
|
-
# 原动作原参数仍可用(前两次失败没有消费掉它)
|
|
57
52
|
assert b.consume(SESSION, ch["nonce"], "skill:mail_send", {"to": "a@x"},
|
|
58
53
|
now=1000.0) == (True, "ok")
|
|
59
54
|
|
|
@@ -83,8 +78,6 @@ def test_pending_is_bounded() -> None:
|
|
|
83
78
|
assert b.pending("nobody") == []
|
|
84
79
|
|
|
85
80
|
|
|
86
|
-
# ── adapter 集成 ───────────────────────────────────────────
|
|
87
|
-
|
|
88
81
|
class _Calls:
|
|
89
82
|
def __init__(self):
|
|
90
83
|
self.n = 0
|
|
@@ -118,7 +111,6 @@ def test_send_requires_nonce_even_when_confirmed():
|
|
|
118
111
|
assert first.confirm_nonce and first.confirm_expires_at > 0
|
|
119
112
|
assert calls.n == 0
|
|
120
113
|
|
|
121
|
-
# 只有 confirmed=True 没有确认码 → 不放行(退回待确认,换发新码)
|
|
122
114
|
no_nonce = adapter.invoke(skill, args, confirmed=True)
|
|
123
115
|
assert no_nonce.status == "needs_confirmation" and calls.n == 0
|
|
124
116
|
assert "missing_nonce" in no_nonce.message
|
|
@@ -128,7 +120,6 @@ def test_send_requires_nonce_even_when_confirmed():
|
|
|
128
120
|
confirm_nonce=no_nonce.confirm_nonce)
|
|
129
121
|
assert ok.success and calls.n == 1
|
|
130
122
|
|
|
131
|
-
# 重放同一个码 → 拒绝,不执行
|
|
132
123
|
replay = adapter.invoke(skill, args, confirmed=True,
|
|
133
124
|
confirm_nonce=no_nonce.confirm_nonce)
|
|
134
125
|
assert replay.status == "needs_confirmation" and calls.n == 1
|
|
@@ -156,13 +147,11 @@ def test_read_skill_and_no_broker_unaffected():
|
|
|
156
147
|
adapter, reg, calls = _build(ConfirmationBroker())
|
|
157
148
|
assert adapter.invoke(reg.get("calc"), {}, confirmed=True).success
|
|
158
149
|
|
|
159
|
-
plain = SkillAdapter(reg)
|
|
150
|
+
plain = SkillAdapter(reg)
|
|
160
151
|
assert plain.invoke(reg.get("mail_send"), {"to": "a@x"}, confirmed=True).success
|
|
161
152
|
assert calls.n >= 1
|
|
162
153
|
|
|
163
154
|
|
|
164
|
-
# ── pipeline 兼容 ──────────────────────────────────────────
|
|
165
|
-
|
|
166
155
|
def test_pipeline_invoke_tolerates_old_adapter():
|
|
167
156
|
"""测试/第三方 adapter 只实现 4 参 invoke 时自动降级(签名探测,不吞内部 TypeError)。"""
|
|
168
157
|
seen = {}
|
|
@@ -26,8 +26,6 @@ def _long_history(n: int = 12, chars: int = 400) -> list:
|
|
|
26
26
|
for i in range(n)]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
# ── 1. Compactor.maybe_compact 契约 ─────────────────────────────
|
|
30
|
-
|
|
31
29
|
def test_maybe_compact_noop_when_under_budget() -> None:
|
|
32
30
|
msgs = [{"role": "user", "content": "短问题"}]
|
|
33
31
|
out, res = Compactor(router=None, max_tokens=6000).maybe_compact(msgs)
|
|
@@ -57,8 +55,6 @@ def test_maybe_compact_never_raises_on_garbage() -> None:
|
|
|
57
55
|
assert isinstance(out, list)
|
|
58
56
|
|
|
59
57
|
|
|
60
|
-
# ── 2. CognitiveAgent._compact_context 接入层 ────────────────────────
|
|
61
|
-
|
|
62
58
|
def _stub_agent(thoughts: list) -> SimpleNamespace:
|
|
63
59
|
return SimpleNamespace(router=None,
|
|
64
60
|
_think=lambda **kw: thoughts.append(kw.get("content", "")))
|
|
@@ -84,10 +80,8 @@ def test_compact_context_swallows_errors(monkeypatch) -> None:
|
|
|
84
80
|
monkeypatch.setenv("FOXCLAW_CONTEXT_MAX_TOKENS", "100")
|
|
85
81
|
msgs = _long_history()
|
|
86
82
|
out = CognitiveAgent._compact_context(SimpleNamespace(router=None), msgs, "chat")
|
|
87
|
-
assert isinstance(out, list)
|
|
88
|
-
|
|
83
|
+
assert isinstance(out, list)
|
|
89
84
|
|
|
90
|
-
# ── 3. 工具调用循环里确实过闸 ───────────────────────────────────
|
|
91
85
|
|
|
92
86
|
class _NoToolRouter:
|
|
93
87
|
"""supports_tools=True 但模型直接给答案:只跑一轮,便于断言压缩闸被调用。"""
|
|
@@ -27,12 +27,10 @@ async def main():
|
|
|
27
27
|
print("=" * 60)
|
|
28
28
|
os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_dispatch_"), shadow_rounds=20)
|
|
29
29
|
|
|
30
|
-
# 1. 主管调度不同类型任务,验证路由
|
|
31
30
|
print("\n【1】主管真实调度(关键词路由)")
|
|
32
31
|
routing_cases = [
|
|
33
32
|
("分析本月家庭支出预算", "finance"),
|
|
34
33
|
("学习一篇关于 RAG 的论文", "office"),
|
|
35
|
-
# 行政并入办公(日程/会议走 office);"整理邮件"不再有助手 Agent,归办公
|
|
36
34
|
("安排周三下午的项目会议", "office"),
|
|
37
35
|
("帮我整理今天的邮件要点", "office"),
|
|
38
36
|
]
|
|
@@ -46,19 +44,16 @@ async def main():
|
|
|
46
44
|
print(f" {'✓' if ok else '✗'} 「{task[:18]}」→ {dispatched} (预期 {expected})")
|
|
47
45
|
check(not bad_routes, f"关键词路由 4/4 命中(失配: {bad_routes or '无'})")
|
|
48
46
|
|
|
49
|
-
# 2. 结果汇聚
|
|
50
47
|
print("\n【2】结果汇聚")
|
|
51
48
|
r = await os_.supervisor.run_task("评估我的投资组合风险")
|
|
52
49
|
res = r.get("result", {})
|
|
53
50
|
print(f" 派给: {res.get('dispatched_to')}")
|
|
54
51
|
print(f" 汇聚: {str(res.get('sub_result'))[:50]}...")
|
|
55
52
|
print(f" 主管摘要: {res.get('summary', '')}")
|
|
56
|
-
# 主管必须真的派出去并拿回子结果,而不是空转返回一句套话
|
|
57
53
|
check(res.get("dispatched_to") == "finance",
|
|
58
54
|
f"投资组合风险任务派给 finance(实为 {res.get('dispatched_to')})")
|
|
59
55
|
check(bool(str(res.get("sub_result") or "").strip()), "子 Agent 结果已汇聚回主管(非空)")
|
|
60
56
|
|
|
61
|
-
# 3. 共享记忆:其他 Agent 能召回主管的调度记录
|
|
62
57
|
print("\n【3】共享记忆层(所有 Agent 可见)")
|
|
63
58
|
empty_shared = []
|
|
64
59
|
for viewer in ["office", "finance", "supervisor"]:
|
|
@@ -69,7 +64,6 @@ async def main():
|
|
|
69
64
|
print(f" {viewer:<10} 召回共享调度记录: {len(shared_hits)} 条")
|
|
70
65
|
check(not empty_shared, f"三个 Agent 都能召回共享调度记录(召回为空: {empty_shared or '无'})")
|
|
71
66
|
|
|
72
|
-
# 4. 隔离仍成立:私有记忆不跨 Agent
|
|
73
67
|
print("\n【4】私有记忆隔离(共享层不打破隔离)")
|
|
74
68
|
await os_.finance.run_task("记录我的私人理财偏好")
|
|
75
69
|
office_view = os_.hmr.recall(query="理财 偏好", agent_id="office", top_k=10)
|