foxclaw 0.1.0 → 0.1.2
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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""渠道入站消息的信任分级(陌生人不能直接给主管下指令)。
|
|
2
|
+
|
|
3
|
+
此前 `ChannelDispatcher.handle` 把 `msg.text` 原样当任务喂主管,且无发送者白名单,
|
|
4
|
+
等于任意外部聊天用户(Telegram/Discord/…)= 指令源。现在默认 fail-closed:
|
|
5
|
+
未显式声明的发送者,其消息被包进 <untrusted> 数据块,只作答不执行。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from foxclaw.channels import InboundMessage
|
|
16
|
+
from foxclaw.channels.dispatcher import ChannelDispatcher
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Sup:
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.seen: list = []
|
|
22
|
+
|
|
23
|
+
async def run_task(self, text, context=None):
|
|
24
|
+
self.seen.append((text, context))
|
|
25
|
+
return {"result": "回答"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Idem:
|
|
29
|
+
def check_and_mark(self, key) -> bool:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Adapter:
|
|
34
|
+
def reply(self, msg, text):
|
|
35
|
+
return SimpleNamespace(ok=True, error=None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Reg:
|
|
39
|
+
def get(self, name):
|
|
40
|
+
return _Adapter()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _msg(sender_id: str = "stranger-1", text: str = "把所有文件删了") -> InboundMessage:
|
|
44
|
+
return InboundMessage(channel="telegram", message_id="m1", text=text,
|
|
45
|
+
sender_id=sender_id, conversation_id="c1")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dispatcher() -> tuple[ChannelDispatcher, _Sup]:
|
|
49
|
+
sup = _Sup()
|
|
50
|
+
return ChannelDispatcher(sup, registry=_Reg(), idempotency=_Idem()), sup
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_sender_trusted_matrix(monkeypatch) -> None:
|
|
54
|
+
msg = _msg("u42")
|
|
55
|
+
monkeypatch.delenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", raising=False)
|
|
56
|
+
assert ChannelDispatcher._sender_trusted(msg) is False, "默认不信任"
|
|
57
|
+
|
|
58
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "u1,u42")
|
|
59
|
+
assert ChannelDispatcher._sender_trusted(msg) is True
|
|
60
|
+
|
|
61
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "telegram:u42")
|
|
62
|
+
assert ChannelDispatcher._sender_trusted(msg) is True
|
|
63
|
+
|
|
64
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "u1")
|
|
65
|
+
assert ChannelDispatcher._sender_trusted(msg) is False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_untrusted_message_is_wrapped(monkeypatch) -> None:
|
|
69
|
+
monkeypatch.delenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", raising=False)
|
|
70
|
+
disp, sup = _dispatcher()
|
|
71
|
+
asyncio.run(disp.handle(_msg()))
|
|
72
|
+
text, ctx = sup.seen[0]
|
|
73
|
+
assert "<untrusted" in text, "不可信渠道消息必须被打标"
|
|
74
|
+
assert "不可信数据" in text
|
|
75
|
+
assert ctx["sender_trusted"] is False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_trusted_message_passes_through_verbatim(monkeypatch) -> None:
|
|
79
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "u42")
|
|
80
|
+
disp, sup = _dispatcher()
|
|
81
|
+
asyncio.run(disp.handle(_msg("u42", "帮我看看今天的日程")))
|
|
82
|
+
text, ctx = sup.seen[0]
|
|
83
|
+
assert text == "帮我看看今天的日程"
|
|
84
|
+
assert "<untrusted" not in text
|
|
85
|
+
assert ctx["sender_trusted"] is True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("raw", ["telegram:*"])
|
|
89
|
+
def test_channel_wildcard_trusts_channel(monkeypatch, raw) -> None:
|
|
90
|
+
"""`channel:*` = 该渠道下所有发送者可信(推荐写法,范围一眼可见)。"""
|
|
91
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", raw)
|
|
92
|
+
assert ChannelDispatcher._sender_trusted(_msg("anyone")) is True
|
|
93
|
+
assert ChannelDispatcher._sender_trusted(
|
|
94
|
+
InboundMessage(channel="wechat", message_id="m1", text="x",
|
|
95
|
+
sender_id="anyone", conversation_id="c1")) is False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_bare_wildcard_requires_explicit_optin(monkeypatch) -> None:
|
|
99
|
+
"""裸 `*` 不再等于「谁都可信」:必须 FOXCLAW_CHANNEL_TRUST_ALL=1 显式确认。
|
|
100
|
+
|
|
101
|
+
它看起来和 `u42` 差不多长,实际是「所有渠道所有发送者」——一次手滑就把外部用户
|
|
102
|
+
全提成指令源,而配置里读不出这个差别。
|
|
103
|
+
"""
|
|
104
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "*")
|
|
105
|
+
monkeypatch.delenv("FOXCLAW_CHANNEL_TRUST_ALL", raising=False)
|
|
106
|
+
assert ChannelDispatcher._sender_trusted(_msg("anyone")) is False
|
|
107
|
+
|
|
108
|
+
monkeypatch.setenv("FOXCLAW_CHANNEL_TRUST_ALL", "1")
|
|
109
|
+
assert ChannelDispatcher._sender_trusted(_msg("anyone")) is True
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
消息渠道回归测试 — 覆盖渠道抽象层 + 四适配器 + 闭环调度
|
|
3
|
+
|
|
4
|
+
验证:
|
|
5
|
+
1. 渠道抽象层(标准消息 + 注册表 + handle_raw 异常隔离)
|
|
6
|
+
2. Telegram(消息标准化 + 群聊识别 + 非文本忽略)
|
|
7
|
+
3. 微信(iLink 鉴权头 + context_token + ClientVersion)
|
|
8
|
+
4. 飞书(URL验证 + 加密解密 + 消息标准化)
|
|
9
|
+
5. Discord(Gateway 事件标准化 + bot 消息忽略)
|
|
10
|
+
6. 闭环调度(收→主管→回 + 幂等去重)
|
|
11
|
+
"""
|
|
12
|
+
import sys, os, json, asyncio, hashlib, base64
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(cond, msg):
|
|
18
|
+
if not cond:
|
|
19
|
+
raise AssertionError(f"✗ {msg}")
|
|
20
|
+
print(f" ✓ {msg}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_abstraction():
|
|
24
|
+
print("\n【1】渠道抽象层")
|
|
25
|
+
from foxclaw.channels import ChannelAdapter, InboundMessage, SendResult, ChannelRegistry, OutboundMessage
|
|
26
|
+
# handle_raw 异常隔离:on_message 抛异常不冒泡
|
|
27
|
+
class A(ChannelAdapter):
|
|
28
|
+
CHANNEL = "t"
|
|
29
|
+
def normalize(self, raw):
|
|
30
|
+
return InboundMessage(channel="t", message_id="m", text=raw.get("text", ""),
|
|
31
|
+
sender_id="u", conversation_id="c")
|
|
32
|
+
def send(self, msg): return SendResult(True)
|
|
33
|
+
processed = []
|
|
34
|
+
def handler(m):
|
|
35
|
+
if m.text == "bad": raise RuntimeError("boom")
|
|
36
|
+
processed.append(m.text)
|
|
37
|
+
ad = A(on_message=handler)
|
|
38
|
+
for txt in ["a", "bad", "c"]:
|
|
39
|
+
ad.handle_raw({"text": txt})
|
|
40
|
+
check(processed == ["a", "c"], "handle_raw 异常隔离(单条失败不影响其他)")
|
|
41
|
+
# 注册表
|
|
42
|
+
reg = ChannelRegistry(); reg.register(ad)
|
|
43
|
+
check(reg.get("t") is ad, "渠道注册表")
|
|
44
|
+
check(not reg.send("nope", OutboundMessage("c", "x")).ok, "未注册渠道发送失败")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_telegram():
|
|
48
|
+
print("\n【2】Telegram")
|
|
49
|
+
from foxclaw.channels.telegram import TelegramAdapter
|
|
50
|
+
tg = TelegramAdapter(bot_token="fake")
|
|
51
|
+
m = tg.normalize({"update_id": 1, "message": {
|
|
52
|
+
"message_id": 5, "date": 1718900000, "text": "你好",
|
|
53
|
+
"chat": {"id": 123, "type": "private"},
|
|
54
|
+
"from": {"id": 123, "username": "snowfox"}}})
|
|
55
|
+
check(m.text == "你好" and not m.is_group, "私聊消息标准化")
|
|
56
|
+
g = tg.normalize({"update_id": 2, "message": {
|
|
57
|
+
"message_id": 6, "date": 1, "text": "hi",
|
|
58
|
+
"chat": {"id": -100, "type": "supergroup"}, "from": {"id": 9}}})
|
|
59
|
+
check(g.is_group, "群聊识别")
|
|
60
|
+
check(tg.normalize({"update_id": 3, "message": {"sticker": {}, "chat": {"id": 1}}}) is None, "非文本忽略")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_weixin():
|
|
64
|
+
print("\n【3】微信")
|
|
65
|
+
from foxclaw.channels.weixin import WeixinAdapter, ITEM_TEXT, MSG_FROM_USER
|
|
66
|
+
wx = WeixinAdapter(token="t")
|
|
67
|
+
h = wx._headers()
|
|
68
|
+
check(h["AuthorizationType"] == "ilink_bot_token", "iLink 鉴权类型")
|
|
69
|
+
check(h["Authorization"].startswith("Bearer "), "Bearer token")
|
|
70
|
+
check(int(base64.b64decode(h["X-WECHAT-UIN"]).decode()) >= 0, "X-WECHAT-UIN 格式")
|
|
71
|
+
check(wx._build_client_version("2.4.3") == 132099, "ClientVersion 计算")
|
|
72
|
+
# 标准化 + context_token 记忆
|
|
73
|
+
msg = {"message_id": 1, "from_user_id": "u1", "session_id": "s",
|
|
74
|
+
"message_type": MSG_FROM_USER, "context_token": "ctx1",
|
|
75
|
+
"item_list": [{"type": ITEM_TEXT, "text_item": {"text": "你好"}}]}
|
|
76
|
+
inb = wx._normalize_msg(msg)
|
|
77
|
+
check(inb.text == "你好", "消息标准化")
|
|
78
|
+
check(wx._context_tokens.get("u1") == "ctx1", "记住 context_token")
|
|
79
|
+
# bot 回显忽略
|
|
80
|
+
check(wx._normalize_msg({"message_type": 2, "item_list": []}) is None, "bot 回显忽略")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_feishu():
|
|
84
|
+
print("\n【4】飞书")
|
|
85
|
+
from foxclaw.channels.feishu import FeishuAdapter
|
|
86
|
+
fs = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="my_secret_key_123")
|
|
87
|
+
# URL 验证
|
|
88
|
+
r = fs.handle_event({"type": "url_verification", "challenge": "c1"})
|
|
89
|
+
check(r.get("challenge") == "c1", "URL 验证 challenge")
|
|
90
|
+
# 加密解密端到端
|
|
91
|
+
def encrypt(plaintext, key_str):
|
|
92
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
93
|
+
key = hashlib.sha256(key_str.encode()).digest()
|
|
94
|
+
iv = os.urandom(16)
|
|
95
|
+
data = plaintext.encode()
|
|
96
|
+
pad = 16 - len(data) % 16
|
|
97
|
+
data += bytes([pad]) * pad
|
|
98
|
+
enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
|
|
99
|
+
return base64.b64encode(iv + enc.update(data) + enc.finalize()).decode()
|
|
100
|
+
original = {"type": "event", "x": "测试"}
|
|
101
|
+
dec = fs.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123"))
|
|
102
|
+
check(dec == original, "AES 加密解密端到端")
|
|
103
|
+
# 错误密钥明确报错
|
|
104
|
+
fs2 = FeishuAdapter(app_id="x", app_secret="y", encrypt_key="wrong")
|
|
105
|
+
check(fs2.decrypt_event(encrypt(json.dumps(original), "my_secret_key_123")) is None, "错误密钥解密失败")
|
|
106
|
+
# 消息标准化
|
|
107
|
+
event = {"header": {"event_type": "im.message.receive_v1"},
|
|
108
|
+
"event": {"message": {"message_id": "om1", "chat_id": "oc1", "chat_type": "p2p",
|
|
109
|
+
"content": json.dumps({"text": "查日程"}), "create_time": "1718900000000"},
|
|
110
|
+
"sender": {"sender_id": {"open_id": "ou1"}}}}
|
|
111
|
+
inb = fs.normalize(event)
|
|
112
|
+
check(inb.text == "查日程" and not inb.is_group, "消息标准化")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_discord():
|
|
116
|
+
print("\n【5】Discord")
|
|
117
|
+
from foxclaw.channels.discord import DiscordAdapter
|
|
118
|
+
dc = DiscordAdapter(bot_token="fake")
|
|
119
|
+
m = dc.normalize({"op": 0, "t": "MESSAGE_CREATE", "d": {
|
|
120
|
+
"id": "m1", "content": "你好", "channel_id": "c1", "guild_id": "g1",
|
|
121
|
+
"author": {"id": "u1", "username": "alice", "bot": False}}})
|
|
122
|
+
check(m.text == "你好" and m.is_group, "频道消息标准化(群)")
|
|
123
|
+
check(dc.normalize({"op": 0, "t": "MESSAGE_CREATE", "d": {
|
|
124
|
+
"id": "m2", "content": "x", "channel_id": "c",
|
|
125
|
+
"author": {"id": "b", "bot": True}}}) is None, "bot 消息忽略")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_closed_loop():
|
|
129
|
+
print("\n【6】闭环调度 + 幂等去重")
|
|
130
|
+
from foxclaw.channels import ChannelAdapter, InboundMessage, SendResult
|
|
131
|
+
from foxclaw.channels.dispatcher import ChannelDispatcher
|
|
132
|
+
from foxclaw import Foxclaw
|
|
133
|
+
import warnings
|
|
134
|
+
warnings.simplefilter("ignore")
|
|
135
|
+
|
|
136
|
+
class Mock(ChannelAdapter):
|
|
137
|
+
CHANNEL = "mock"
|
|
138
|
+
def __init__(self): super().__init__(); self.sent = []
|
|
139
|
+
def normalize(self, raw): return None
|
|
140
|
+
def send(self, msg): self.sent.append(msg.text); return SendResult(True)
|
|
141
|
+
|
|
142
|
+
import tempfile, shutil
|
|
143
|
+
data_dir = tempfile.mkdtemp(prefix="foxclaw_ch_test_")
|
|
144
|
+
shutil.rmtree(data_dir, ignore_errors=True) # 确保干净起步(防残留幂等记录污染)
|
|
145
|
+
os_ = Foxclaw(data_dir=data_dir, shadow_rounds=50)
|
|
146
|
+
disp = ChannelDispatcher(os_.supervisor)
|
|
147
|
+
mock = Mock(); disp.register_channel(mock)
|
|
148
|
+
|
|
149
|
+
async def go():
|
|
150
|
+
msg = InboundMessage(channel="mock", message_id="m1", text="介绍自己",
|
|
151
|
+
sender_id="u1", conversation_id="c1")
|
|
152
|
+
r1 = await disp.handle(msg)
|
|
153
|
+
check(r1["replied"], "闭环:收→主管→回")
|
|
154
|
+
# 幂等:同消息重发只处理一次
|
|
155
|
+
r2 = await disp.handle(msg)
|
|
156
|
+
check(r2.get("idempotent_skip"), "重发消息被幂等跳过")
|
|
157
|
+
check(len(mock.sent) == 1, "重发只回复一次")
|
|
158
|
+
asyncio.run(go())
|
|
159
|
+
# 清理
|
|
160
|
+
import shutil
|
|
161
|
+
shutil.rmtree(data_dir, ignore_errors=True)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
print("=" * 56)
|
|
166
|
+
print("消息渠道回归测试")
|
|
167
|
+
print("=" * 56)
|
|
168
|
+
tests = [test_abstraction, test_telegram, test_weixin,
|
|
169
|
+
test_feishu, test_discord, test_closed_loop]
|
|
170
|
+
failed = 0
|
|
171
|
+
for t in tests:
|
|
172
|
+
try:
|
|
173
|
+
t()
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f" ✗ {t.__name__} 失败: {e}")
|
|
176
|
+
failed += 1
|
|
177
|
+
print("\n" + "=" * 56)
|
|
178
|
+
print(f"结果:{len(tests) - failed}/{len(tests)} 组通过")
|
|
179
|
+
print("=" * 56)
|
|
180
|
+
sys.exit(1 if failed else 0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""`foxclaw` 命令行入口的最小回归。
|
|
2
|
+
|
|
3
|
+
不启动真实服务(uvicorn 不在测试里跑),只锁住入口名、参数默认值与版本输出——
|
|
4
|
+
这三样是发布到 PyPI/npm 后用户第一眼会碰到的契约。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from foxclaw import __version__
|
|
12
|
+
from foxclaw.cli import APP_TARGET, build_parser, main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_target_is_api_app() -> None:
|
|
16
|
+
assert APP_TARGET == "foxclaw.api:app"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_version_flag(capsys: pytest.CaptureFixture[str]) -> None:
|
|
20
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
21
|
+
main(["--version"])
|
|
22
|
+
assert excinfo.value.code == 0
|
|
23
|
+
out = capsys.readouterr().out
|
|
24
|
+
assert "foxclaw" in out
|
|
25
|
+
assert __version__ in out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_no_args_prints_hint(capsys: pytest.CaptureFixture[str]) -> None:
|
|
29
|
+
assert main([]) == 0
|
|
30
|
+
out = capsys.readouterr().out
|
|
31
|
+
assert __version__ in out
|
|
32
|
+
assert "foxclaw serve" in out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_serve_defaults() -> None:
|
|
36
|
+
args = build_parser().parse_args(["serve"])
|
|
37
|
+
assert args.host == "127.0.0.1"
|
|
38
|
+
assert args.port == 8000
|
|
39
|
+
assert args.reload is False
|
|
40
|
+
assert args.handler is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_serve_overrides() -> None:
|
|
44
|
+
args = build_parser().parse_args(["serve", "--port", "9001", "--reload"])
|
|
45
|
+
assert args.port == 9001
|
|
46
|
+
assert args.reload is True
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""M5 · 上下文压缩 · 工具消息配对保护(回归)。
|
|
2
|
+
|
|
3
|
+
病根:压缩器原按**条数**切 keep_recent,边界若切在一轮 function-calling
|
|
4
|
+
中间,会切出「有 tool 结果、但其 assistant.tool_calls 已被摘要掉」的孤立
|
|
5
|
+
片段 —— 送回 OpenAI/Anthropic 会因孤立 tool_call 引用报 400。
|
|
6
|
+
|
|
7
|
+
实测(修复前):一轮 2 个工具 + keep_recent=3 → 孤立 call_2。
|
|
8
|
+
|
|
9
|
+
本文件锁定 group_tool_calls 的分组正确性 + 各 keep 值下配对完整 +
|
|
10
|
+
畸形输入的安全降级(skipped_unpaired_tool 而非产出非法序列)。
|
|
11
|
+
"""
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.core.compaction import Compactor, group_tool_calls
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check(cond, msg):
|
|
21
|
+
if not cond:
|
|
22
|
+
raise AssertionError(f"✗ {msg}")
|
|
23
|
+
print(f" ✓ {msg}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _tool_msgs():
|
|
27
|
+
"""构造真实工具循环产生的消息形态(参照 base._reply_with_tools:
|
|
28
|
+
每轮 1 个 assistant 带 N 个 tool_calls + N 个 tool 结果)。"""
|
|
29
|
+
return [
|
|
30
|
+
{"role": "system", "content": "系统提示"},
|
|
31
|
+
{"role": "user", "content": "问题" + "x" * 300},
|
|
32
|
+
{"role": "assistant", "content": "", "tool_calls": [
|
|
33
|
+
{"id": "call_1", "type": "function",
|
|
34
|
+
"function": {"name": "web_search", "arguments": "{}"}},
|
|
35
|
+
{"id": "call_2", "type": "function",
|
|
36
|
+
"function": {"name": "recall_memory", "arguments": "{}"}},
|
|
37
|
+
]},
|
|
38
|
+
{"role": "tool", "tool_call_id": "call_1", "content": "结果1" + "y" * 300},
|
|
39
|
+
{"role": "tool", "tool_call_id": "call_2", "content": "结果2" + "z" * 300},
|
|
40
|
+
{"role": "assistant", "content": "答案" + "w" * 300},
|
|
41
|
+
{"role": "user", "content": "继续" + "q" * 300},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pairs(msgs):
|
|
46
|
+
"""返回 (声明的 tool_call_id 集合, 存在的 tool 结果 id 集合)。"""
|
|
47
|
+
declared = {tc["id"] for m in msgs if m.get("tool_calls")
|
|
48
|
+
for tc in m["tool_calls"]}
|
|
49
|
+
got = {m.get("tool_call_id") for m in msgs if m.get("role") == "tool"}
|
|
50
|
+
return declared, got
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
print("=" * 60)
|
|
55
|
+
print("M5 · 压缩器工具消息配对保护")
|
|
56
|
+
print("=" * 60)
|
|
57
|
+
|
|
58
|
+
print("\n【1】原子组划分:assistant 与其 tool 结果同组")
|
|
59
|
+
groups = group_tool_calls(_tool_msgs())
|
|
60
|
+
tool_groups = [g for g in groups
|
|
61
|
+
if any(m.get("role") == "assistant" and m.get("tool_calls")
|
|
62
|
+
for m in g)]
|
|
63
|
+
check(len(tool_groups) == 1,
|
|
64
|
+
f"含 tool_calls 的组应唯一(实际 {len(tool_groups)})")
|
|
65
|
+
check(len(tool_groups[0]) == 3,
|
|
66
|
+
f"该组应含 assistant + 2 个 tool 结果(实际 {len(tool_groups[0])} 条)"
|
|
67
|
+
f"→ {[m.get('role') for m in tool_groups[0]]}")
|
|
68
|
+
check(sum(len(g) for g in groups) == len(_tool_msgs()),
|
|
69
|
+
"分组不丢消息(总数守恒)")
|
|
70
|
+
|
|
71
|
+
print("\n【2】各 keep_recent 值下配对均完整(修复前 keep=3 必断)")
|
|
72
|
+
for keep in (1, 2, 3, 4):
|
|
73
|
+
c = Compactor(router=None, max_tokens=10, keep_recent=keep)
|
|
74
|
+
r = c.compact(_tool_msgs())
|
|
75
|
+
declared, got = _pairs(r.messages)
|
|
76
|
+
orphan, missing = got - declared, declared - got
|
|
77
|
+
check(not orphan and not missing,
|
|
78
|
+
f"keep={keep} 配对完整(method={r.method}"
|
|
79
|
+
f"{f' orphan={orphan}' if orphan else ''}"
|
|
80
|
+
f"{f' missing={missing}' if missing else ''})")
|
|
81
|
+
|
|
82
|
+
print("\n【3】畸形输入(孤儿 tool)安全降级,不产出非法序列")
|
|
83
|
+
bad = [{"role": "system", "content": "s"},
|
|
84
|
+
{"role": "tool", "tool_call_id": "ghost", "content": "孤儿"},
|
|
85
|
+
{"role": "user", "content": "u" * 300}]
|
|
86
|
+
r = Compactor(router=None, max_tokens=5, keep_recent=1).compact(bad)
|
|
87
|
+
check(r.method in ("skipped_unpaired_tool", "none", "skipped_ineffective"),
|
|
88
|
+
f"孤儿输入走安全降级(method={r.method})")
|
|
89
|
+
check(len(r.messages) == len(bad),
|
|
90
|
+
"安全降级时原样返回,不丢消息")
|
|
91
|
+
|
|
92
|
+
print("\n【4】无工具消息的普通对话不受影响(不回归)")
|
|
93
|
+
plain = [{"role": "system", "content": "s"},
|
|
94
|
+
{"role": "user", "content": "你好" + "a" * 400},
|
|
95
|
+
{"role": "assistant", "content": "回答" + "b" * 400},
|
|
96
|
+
{"role": "user", "content": "再问" + "c" * 400},
|
|
97
|
+
{"role": "assistant", "content": "再答" + "d" * 400}]
|
|
98
|
+
c = Compactor(router=None, max_tokens=10, keep_recent=2)
|
|
99
|
+
r = c.compact(plain)
|
|
100
|
+
check(r.messages, "普通对话压缩后非空")
|
|
101
|
+
d, g = _pairs(r.messages)
|
|
102
|
+
check(not (g - d), "普通对话不产生孤立 tool(无 tool 消息)")
|
|
103
|
+
|
|
104
|
+
print("\n【5】现有 compaction 行为保持(test_governance 覆盖)")
|
|
105
|
+
r2 = Compactor(router=None, max_tokens=6000, keep_recent=4).compact(plain)
|
|
106
|
+
check(r2.method == "none",
|
|
107
|
+
f"未超阈值时不压缩(method={r2.method})")
|
|
108
|
+
|
|
109
|
+
print("\n" + "=" * 60)
|
|
110
|
+
print("M5 全部通过")
|
|
111
|
+
print("=" * 60)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_compaction_toolpair():
|
|
115
|
+
main()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""E1:分层配置 —— LayeredConfig 三层合并 + SkillPolicy policy 层 deny(管理员锁定,不可覆盖)。"""
|
|
2
|
+
import os, sys, tempfile, shutil, json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_config_layering():
|
|
8
|
+
print("=" * 60); print("E1 分层配置验证"); print("=" * 60)
|
|
9
|
+
|
|
10
|
+
from foxclaw.core.config import LayeredConfig, load_policy_layer
|
|
11
|
+
|
|
12
|
+
# 1) LayeredConfig:标量高层覆盖;集合三层并集(只增不减);policy_locked
|
|
13
|
+
cfg = LayeredConfig(policy={"model": "P", "deny": ["a"]},
|
|
14
|
+
user={"model": "U", "deny": ["b"]},
|
|
15
|
+
runtime={"model": "R", "deny": ["c"], "x": 1})
|
|
16
|
+
assert cfg.get("model") == "P", "标量:policy 覆盖 user/runtime"
|
|
17
|
+
assert cfg.get("x") == 1, "低层独有键仍可读"
|
|
18
|
+
assert cfg.merged_set("deny") == {"a", "b", "c"}, "集合:三层并集(只增不减)"
|
|
19
|
+
assert cfg.policy_locked("deny") == {"a"}, "policy 层锁定项"
|
|
20
|
+
print("[1] LayeredConfig 标量覆盖 + 集合并集 + policy_locked OK")
|
|
21
|
+
|
|
22
|
+
# 2) load_policy_layer:env + policy.json 合并;默认空
|
|
23
|
+
d = tempfile.mkdtemp(prefix="e1_")
|
|
24
|
+
try:
|
|
25
|
+
os.environ["FOXCLAW_POLICY_DENY"] = "gmail_send, danger_tool"
|
|
26
|
+
(Path(d) / "policy.json").write_text(json.dumps({"deny": ["file_delete"]}), encoding="utf-8")
|
|
27
|
+
pl = load_policy_layer(d)
|
|
28
|
+
assert set(pl["deny"]) == {"gmail_send", "danger_tool", "file_delete"}, pl
|
|
29
|
+
print(f"[2] load_policy_layer(env + policy.json) = {pl['deny']} OK")
|
|
30
|
+
|
|
31
|
+
# 3) SkillPolicy:policy 层 deny 生效 + undeny 撤不掉 + 从盘 user deny 生效(引用未断) + user 可撤
|
|
32
|
+
(Path(d) / "skill_policy.json").write_text(
|
|
33
|
+
json.dumps({"deny": ["user_denied"], "allow": []}), encoding="utf-8")
|
|
34
|
+
from foxclaw.skills.policy import SkillPolicy
|
|
35
|
+
pol = SkillPolicy(d)
|
|
36
|
+
assert pol.is_blocked("gmail_send") is True, "policy 层 deny 生效"
|
|
37
|
+
assert pol.is_blocked("user_denied") is True, "从盘加载的 user deny 生效(_load 就地更新、引用未断)"
|
|
38
|
+
assert pol.undeny("gmail_send") is False, "policy 层 deny 不可被 undeny 撤销(管理员锁定)"
|
|
39
|
+
assert pol.is_blocked("gmail_send") is True, "undeny 后 policy 层 deny 仍在"
|
|
40
|
+
assert pol.undeny("user_denied") is True and pol.is_blocked("user_denied") is False, "user 层 deny 可撤"
|
|
41
|
+
snap = pol.snapshot()
|
|
42
|
+
assert set(snap["policy_deny"]) == {"gmail_send", "danger_tool", "file_delete"}, snap
|
|
43
|
+
print("[3] SkillPolicy:policy 层不可 undeny + user 层可撤 + 从盘 user deny 生效 OK")
|
|
44
|
+
finally:
|
|
45
|
+
os.environ.pop("FOXCLAW_POLICY_DENY", None)
|
|
46
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
47
|
+
|
|
48
|
+
# 4) behavior-preserving:无 policy 层(无 env、无 policy.json)→ SkillPolicy 行为 = 现状
|
|
49
|
+
d2 = tempfile.mkdtemp(prefix="e1b_")
|
|
50
|
+
try:
|
|
51
|
+
from foxclaw.skills.policy import SkillPolicy
|
|
52
|
+
pol2 = SkillPolicy(d2)
|
|
53
|
+
assert pol2.is_blocked("anything") is False, "无 policy 层 + 无 user deny → 不拦"
|
|
54
|
+
pol2.deny("x"); assert pol2.is_blocked("x") is True
|
|
55
|
+
assert pol2.undeny("x") is True and pol2.is_blocked("x") is False, "user deny/undeny 照常(行为不变)"
|
|
56
|
+
assert pol2.snapshot()["policy_deny"] == [], "无 policy 层 → policy_deny 空"
|
|
57
|
+
print("[4] 无 policy 层 → SkillPolicy 行为 = 现状(behavior-preserving) OK")
|
|
58
|
+
finally:
|
|
59
|
+
shutil.rmtree(d2, ignore_errors=True)
|
|
60
|
+
|
|
61
|
+
print("\nE1 分层配置验证通过")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
test_config_layering()
|