foxclaw 0.1.0 → 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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -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 +115 -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 +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -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 +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -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 +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -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 +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -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 +144 -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 +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -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 +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -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 +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -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 +1436 -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-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -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 +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- 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 +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -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 +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -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 +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -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 +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -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,256 @@
|
|
|
1
|
+
"""请求链路追踪:一个 trace_id 贯穿 请求→Agent→LLM,带 span 耗时;有界环存;无 trace 零成本。
|
|
2
|
+
与 METRICS(聚合)分工:traces 回答"**这一次**慢在哪、调了谁"。全离线(stub)。"""
|
|
3
|
+
import os, sys, time, json, threading, tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
|
|
7
|
+
def test_tracing():
|
|
8
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
9
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
10
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
11
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_trace_")
|
|
12
|
+
for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"):
|
|
13
|
+
os.environ.pop(k, None)
|
|
14
|
+
|
|
15
|
+
from foxclaw.core.tracing import (Trace, TraceStore, start_trace, current_trace,
|
|
16
|
+
clear_trace, span, MAX_SPANS_PER_TRACE)
|
|
17
|
+
|
|
18
|
+
print("=" * 60); print("请求链路追踪验证"); print("=" * 60)
|
|
19
|
+
|
|
20
|
+
clear_trace()
|
|
21
|
+
assert current_trace() is None
|
|
22
|
+
with span("orphan", x=1) as s:
|
|
23
|
+
assert s is None
|
|
24
|
+
print("[1] 无 trace 时 span no-op(不报错/不产生垃圾)OK")
|
|
25
|
+
|
|
26
|
+
tr = start_trace("unit")
|
|
27
|
+
with span("outer", k="v"):
|
|
28
|
+
with span("inner"):
|
|
29
|
+
time.sleep(0.01)
|
|
30
|
+
d = tr.to_dict()
|
|
31
|
+
names = [s["name"] for s in d["spans"]]
|
|
32
|
+
assert names == ["inner", "outer"], names
|
|
33
|
+
inner = d["spans"][0]
|
|
34
|
+
assert inner["ok"] is True and inner["ms"] >= 9, inner
|
|
35
|
+
assert d["spans"][1]["meta"] == {"k": "v"}
|
|
36
|
+
print(f"[2] span 记录(嵌套/耗时 {inner['ms']}ms/meta)OK")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with span("boom"):
|
|
40
|
+
raise ValueError("kaboom")
|
|
41
|
+
raise AssertionError("异常应原样抛出")
|
|
42
|
+
except ValueError:
|
|
43
|
+
pass
|
|
44
|
+
bad = [s for s in tr.to_dict()["spans"] if s["name"] == "boom"][0]
|
|
45
|
+
assert bad["ok"] is False and "kaboom" in bad["error"], bad
|
|
46
|
+
print("[3] span 异常记录 ok=False + 原样抛出 OK")
|
|
47
|
+
|
|
48
|
+
t2 = Trace("cap")
|
|
49
|
+
for i in range(MAX_SPANS_PER_TRACE + 50):
|
|
50
|
+
t2.add_span({"name": f"s{i}"})
|
|
51
|
+
assert len(t2.to_dict()["spans"]) == MAX_SPANS_PER_TRACE and t2.to_dict()["dropped_spans"] == 50
|
|
52
|
+
print(f"[4] 单 trace span 封顶 {MAX_SPANS_PER_TRACE}(超出计 dropped)OK")
|
|
53
|
+
|
|
54
|
+
st = TraceStore(limit=3)
|
|
55
|
+
ids = []
|
|
56
|
+
for i in range(5):
|
|
57
|
+
t = Trace(f"t{i}"); t.finish(); st.put(t); ids.append(t.trace_id)
|
|
58
|
+
assert len(st.list(limit=99)) == 3, "环存未封顶"
|
|
59
|
+
assert st.get(ids[-1]) is not None and st.get(ids[0]) is None, "应只留最近 3 条"
|
|
60
|
+
print("[5] TraceStore 有界环存(只留最近 N)+ 按 id 取 OK")
|
|
61
|
+
|
|
62
|
+
clear_trace()
|
|
63
|
+
res = {}
|
|
64
|
+
def worker(name):
|
|
65
|
+
t = start_trace(name)
|
|
66
|
+
with span("work"):
|
|
67
|
+
time.sleep(0.01)
|
|
68
|
+
res[name] = (t.trace_id, [s["name"] for s in t.to_dict()["spans"]])
|
|
69
|
+
ths = [threading.Thread(target=worker, args=(f"w{i}",)) for i in range(3)]
|
|
70
|
+
[t.start() for t in ths]; [t.join() for t in ths]
|
|
71
|
+
assert len(res) == 3 and len({v[0] for v in res.values()}) == 3, res
|
|
72
|
+
assert all(v[1] == ["work"] for v in res.values()), "各线程 span 应各归各的 trace"
|
|
73
|
+
print("[6] 线程隔离(3 线程 3 条独立 trace,span 不串)OK")
|
|
74
|
+
|
|
75
|
+
from fastapi.testclient import TestClient
|
|
76
|
+
from foxclaw.api import app
|
|
77
|
+
|
|
78
|
+
with TestClient(app) as c:
|
|
79
|
+
r = c.post("/api/task", json={"task": "追踪自测", "agent_id": "office"})
|
|
80
|
+
assert r.status_code == 200, r.status_code
|
|
81
|
+
tid = r.headers.get("X-Trace-Id")
|
|
82
|
+
assert tid, "响应应带 X-Trace-Id"
|
|
83
|
+
detail = c.get(f"/api/traces/{tid}").json()
|
|
84
|
+
assert detail["trace_id"] == tid and detail["duration_ms"] > 0, detail
|
|
85
|
+
names = [s["name"] for s in detail["spans"]]
|
|
86
|
+
llm = [n for n in names if n.startswith("llm.")]
|
|
87
|
+
assert llm, f"链路里应有 LLM span: {names}"
|
|
88
|
+
mem = [n for n in names if n.startswith("memory.")]
|
|
89
|
+
assert mem, f"跨线程的 Agent 记忆 span 应归属本 trace(contextvars 传播): {names}"
|
|
90
|
+
assert all(s["ms"] >= 0 for s in detail["spans"]), "每个 span 都应有耗时"
|
|
91
|
+
stages = [n for n in names if n.startswith("stage.")]
|
|
92
|
+
for must in ("stage.recall", "stage.act", "stage.verify"):
|
|
93
|
+
assert must in names, f"缺 {must},埋点覆盖退化了: {names}"
|
|
94
|
+
assert len(detail["spans"]) >= 6, f"链路过稀(疑似埋点退化): {names}"
|
|
95
|
+
print(f"[7] 请求→trace + X-Trace-Id;{detail['span_count']} span "
|
|
96
|
+
f"(管线 {len(stages)} stage + 跨线程 {mem[0]})OK")
|
|
97
|
+
|
|
98
|
+
lst = c.get("/api/traces?limit=5").json()["traces"]
|
|
99
|
+
assert any(t["trace_id"] == tid for t in lst), "最近列表应含刚才那条"
|
|
100
|
+
assert "spans" not in lst[0], "列表是摘要,不带 spans(省负载)"
|
|
101
|
+
assert c.get("/api/traces/deadbeef0000").status_code == 404
|
|
102
|
+
print(f"[8] /api/traces 列表({len(lst)} 条摘要)+ 未知 id 404 OK")
|
|
103
|
+
|
|
104
|
+
before = len(c.get("/api/traces?limit=200").json()["traces"])
|
|
105
|
+
c.get("/health"); c.get("/metrics")
|
|
106
|
+
after = c.get("/api/traces?limit=200").json()["traces"]
|
|
107
|
+
assert not any(t["name"].endswith("/health") for t in after), "探针不该被追踪"
|
|
108
|
+
print("[9] 探针 /health、/metrics 不进 trace OK")
|
|
109
|
+
|
|
110
|
+
import time as _t
|
|
111
|
+
store = TraceStore(limit=2)
|
|
112
|
+
store.attach_db(Path(os.environ["FOXCLAW_DATA_DIR"]) / "t.db")
|
|
113
|
+
|
|
114
|
+
def _mk(name, ms, err=False):
|
|
115
|
+
tr = Trace(name); tr.add_span({"name": "s", "ok": not err, "ms": ms})
|
|
116
|
+
tr.duration_ms = ms; tr.start_ts = _t.time()
|
|
117
|
+
return tr
|
|
118
|
+
|
|
119
|
+
os.environ["FOXCLAW_TRACE_SLOW_MS"] = "500"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
|
|
120
|
+
slow = _mk("GET /slow", 900); fast = _mk("GET /fast", 5); bad = _mk("GET /err", 5, err=True)
|
|
121
|
+
for t in (slow, fast, bad):
|
|
122
|
+
store.put(t)
|
|
123
|
+
ids = {r["trace_id"] for r in store.query(limit=99)}
|
|
124
|
+
assert slow.trace_id in ids, "慢请求必须落库"
|
|
125
|
+
assert bad.trace_id in ids, "出错请求必须落库"
|
|
126
|
+
assert fast.trace_id not in ids, "正常请求(采样率0)不该落库"
|
|
127
|
+
print("[10] 采样策略(慢/出错必存、正常按采样率不存)OK")
|
|
128
|
+
|
|
129
|
+
for i in range(5):
|
|
130
|
+
store.put(_mk(f"GET /x{i}", 1))
|
|
131
|
+
assert store.get(slow.trace_id) is None, "应已滚出内存环"
|
|
132
|
+
d = store.get_dict(slow.trace_id)
|
|
133
|
+
assert d and d.get("persisted") and d["duration_ms"] == 900 and d["spans"], d
|
|
134
|
+
print("[11] 内存环滚出后仍可从库捞回(重启/滚动后可查历史)OK")
|
|
135
|
+
|
|
136
|
+
assert all(r["duration_ms"] >= 500 for r in store.query(limit=99, slow_ms=500))
|
|
137
|
+
errs = store.query(limit=99, errors_only=True)
|
|
138
|
+
assert errs and all(r["ok"] is False for r in errs), errs
|
|
139
|
+
print("[12] 历史查询过滤(slow_ms / errors_only)OK")
|
|
140
|
+
|
|
141
|
+
os.environ["FOXCLAW_TRACE_RETAIN"] = "100"; os.environ["FOXCLAW_TRACE_SAMPLE"] = "1"
|
|
142
|
+
for i in range(140):
|
|
143
|
+
store.put(_mk(f"GET /bulk{i}", 1))
|
|
144
|
+
assert len(store.query(limit=999)) <= 100, "落库未按保留上限修剪"
|
|
145
|
+
os.environ["FOXCLAW_TRACE_SAMPLE"] = "0"
|
|
146
|
+
print("[13] 落库保留上限(超出删最旧,磁盘有界)OK")
|
|
147
|
+
|
|
148
|
+
s2 = TraceStore(limit=2)
|
|
149
|
+
s2.attach_db("/nonexistent-dir-xyz/なし/t.db")
|
|
150
|
+
s2.put(_mk("GET /ok", 900))
|
|
151
|
+
assert s2.query(limit=9) == [] and len(s2.list(limit=9)) == 1
|
|
152
|
+
print("[14] 库不可用 → 降级仅内存,不影响请求 OK")
|
|
153
|
+
|
|
154
|
+
from foxclaw.core.tracing import parse_traceparent, to_traceparent, current_traceparent
|
|
155
|
+
|
|
156
|
+
good = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
|
157
|
+
assert parse_traceparent(good) == "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
158
|
+
assert parse_traceparent("00-" + "0" * 32 + "-00f067aa0ba902b7-01") is None, "全零 trace-id 非法"
|
|
159
|
+
for bad_tp in (None, "", "garbage", "00-short-00f067aa0ba902b7-01", "99-" + "a" * 32 + "-" + "b" * 16 + "-01"):
|
|
160
|
+
assert parse_traceparent(bad_tp) is None, bad_tp
|
|
161
|
+
print("[15] traceparent 解析(合法接受/非法+全零拒绝)OK")
|
|
162
|
+
|
|
163
|
+
t16 = Trace("w3c")
|
|
164
|
+
assert len(t16.trace_id) == 32 and all(c in "0123456789abcdef" for c in t16.trace_id), t16.trace_id
|
|
165
|
+
tp = to_traceparent(t16.trace_id)
|
|
166
|
+
assert parse_traceparent(tp) == t16.trace_id, "自己生成的应能被自己解析(自洽)"
|
|
167
|
+
print("[16] trace_id 32hex 合规 + 出站 traceparent 自洽 OK")
|
|
168
|
+
|
|
169
|
+
with TestClient(app) as c:
|
|
170
|
+
r = c.post("/api/task", json={"task": "接链自测", "agent_id": "supervisor"},
|
|
171
|
+
headers={"traceparent": good})
|
|
172
|
+
assert r.headers.get("X-Trace-Id") == "4bf92f3577b34da6a3ce929d0e0e4736", r.headers.get("X-Trace-Id")
|
|
173
|
+
r2 = c.post("/api/task", json={"task": "非法头", "agent_id": "supervisor"},
|
|
174
|
+
headers={"traceparent": "garbage"})
|
|
175
|
+
assert r2.headers.get("X-Trace-Id") and r2.headers["X-Trace-Id"] != "garbage"
|
|
176
|
+
print("[17] 入站接链(合法 traceparent 复用其 trace_id;非法则自开)OK")
|
|
177
|
+
|
|
178
|
+
clear_trace()
|
|
179
|
+
assert current_traceparent() is None
|
|
180
|
+
t18 = start_trace("out")
|
|
181
|
+
assert parse_traceparent(current_traceparent()) == t18.trace_id
|
|
182
|
+
clear_trace()
|
|
183
|
+
print("[18] 出站 current_traceparent(trace 内可用/trace 外 None)OK")
|
|
184
|
+
|
|
185
|
+
from foxclaw.core.tracing import OTLP, _trace_to_otlp
|
|
186
|
+
|
|
187
|
+
os.environ.pop("FOXCLAW_OTLP_ENDPOINT", None)
|
|
188
|
+
assert OTLP.stats()["enabled"] is False
|
|
189
|
+
OTLP.submit(_mk("GET /x", 5))
|
|
190
|
+
print("[19] 未配 endpoint → OTLP 完全不启用(零成本)OK")
|
|
191
|
+
|
|
192
|
+
t20 = start_trace("GET /demo")
|
|
193
|
+
with span("child.ok"): pass
|
|
194
|
+
try:
|
|
195
|
+
with span("child.bad"):
|
|
196
|
+
raise RuntimeError("x")
|
|
197
|
+
except RuntimeError:
|
|
198
|
+
pass
|
|
199
|
+
t20.finish(); clear_trace()
|
|
200
|
+
o = _trace_to_otlp(t20)
|
|
201
|
+
sp = o["resourceSpans"][0]["scopeSpans"][0]["spans"]
|
|
202
|
+
root = sp[0]; kids = sp[1:]
|
|
203
|
+
assert len(root["traceId"]) == 32 and root["name"] == "GET /demo" and root["kind"] == 2
|
|
204
|
+
assert all(k["parentSpanId"] == root["spanId"] for k in kids), "子 span 应挂在根 span 下"
|
|
205
|
+
assert all(k["traceId"] == t20.trace_id for k in kids), "同一 trace 的 span traceId 必须一致"
|
|
206
|
+
assert all(int(k["endTimeUnixNano"]) >= int(k["startTimeUnixNano"]) for k in sp), "结束不得早于开始"
|
|
207
|
+
bad = [k for k in kids if k["name"] == "child.bad"][0]
|
|
208
|
+
assert bad["status"]["code"] == 2 and root["status"]["code"] == 2, "出错应标 ERROR 并冒泡到根"
|
|
209
|
+
ok = [k for k in kids if k["name"] == "child.ok"][0]
|
|
210
|
+
assert ok["status"]["code"] == 1
|
|
211
|
+
svc = o["resourceSpans"][0]["resource"]["attributes"][0]
|
|
212
|
+
assert svc["key"] == "service.name" and svc["value"]["stringValue"] == "foxclaw"
|
|
213
|
+
print(f"[20] OTLP/JSON 结构(根+{len(kids)} 子 span·parentSpanId·32hex traceId·状态码)OK")
|
|
214
|
+
|
|
215
|
+
import http.server, socketserver, threading as _th2
|
|
216
|
+
received = []
|
|
217
|
+
class _H(http.server.BaseHTTPRequestHandler):
|
|
218
|
+
def do_POST(self):
|
|
219
|
+
n = int(self.headers.get("Content-Length", 0))
|
|
220
|
+
received.append((self.path, json.loads(self.rfile.read(n) or b"{}")))
|
|
221
|
+
self.send_response(200); self.end_headers(); self.wfile.write(b"{}")
|
|
222
|
+
def log_message(self, *a): pass
|
|
223
|
+
srv = socketserver.TCPServer(("127.0.0.1", 0), _H)
|
|
224
|
+
port = srv.server_address[1]
|
|
225
|
+
_th2.Thread(target=srv.serve_forever, daemon=True).start()
|
|
226
|
+
os.environ["FOXCLAW_OTLP_ENDPOINT"] = f"http://127.0.0.1:{port}"
|
|
227
|
+
t21 = _mk("GET /exported", 12)
|
|
228
|
+
OTLP.submit(t21)
|
|
229
|
+
for _ in range(50):
|
|
230
|
+
if received: break
|
|
231
|
+
_t.sleep(0.1)
|
|
232
|
+
srv.shutdown()
|
|
233
|
+
assert received, "collector 未收到 OTLP 上报"
|
|
234
|
+
path, body = received[0]
|
|
235
|
+
assert path == "/v1/traces", path
|
|
236
|
+
got = body["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
|
|
237
|
+
assert got["traceId"] == t21.trace_id and got["name"] == "GET /exported", got
|
|
238
|
+
print("[21] 端到端导出(本地 collector 收到 POST /v1/traces,traceId 对得上)OK")
|
|
239
|
+
|
|
240
|
+
os.environ["FOXCLAW_OTLP_ENDPOINT"] = "http://127.0.0.1:1"
|
|
241
|
+
from foxclaw.core.tracing import _OtlpExporter
|
|
242
|
+
ex = _OtlpExporter(maxsize=2)
|
|
243
|
+
before = ex.dropped
|
|
244
|
+
t0_ = _t.perf_counter()
|
|
245
|
+
for i in range(50):
|
|
246
|
+
ex.submit(_mk(f"GET /f{i}", 1))
|
|
247
|
+
elapsed = (_t.perf_counter() - t0_) * 1000
|
|
248
|
+
assert ex.dropped > before, "队列满时应丢弃而非阻塞"
|
|
249
|
+
assert elapsed < 2000, f"submit 不该阻塞调用方(耗时 {elapsed:.0f}ms)"
|
|
250
|
+
os.environ.pop("FOXCLAW_OTLP_ENDPOINT", None)
|
|
251
|
+
print(f"[22] 队列满→丢弃 {ex.dropped} 条且不阻塞(50 次 submit 仅 {elapsed:.0f}ms)OK")
|
|
252
|
+
|
|
253
|
+
print("\n请求链路追踪验证全部通过")
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
test_tracing()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
真值走 HKC/KEE(v2 ④ Step1)测试 —— 全离线,假 HKC(不调 LLM)。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 默认关:on_truth 不结晶(行为不变)
|
|
6
|
+
2. 启用:后台把真值 fact 经 hkc.client.ingest_text(KDE→KEE)结晶
|
|
7
|
+
3. 被拦截任务跳过 / HKC 不可用跳过(不崩)
|
|
8
|
+
4. 真实接线:Foxclaw 把结晶器订阅到 truth.appended;发该事件即触发(启用+假HKC)
|
|
9
|
+
"""
|
|
10
|
+
import os, sys, asyncio, tempfile, shutil, types
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
13
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
14
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.knowledge import TruthCrystallizer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Client:
|
|
21
|
+
def __init__(self): self.ingested = []
|
|
22
|
+
def ingest_text(self, text, source_title="", domain="", **kw):
|
|
23
|
+
self.ingested.append((text, domain)); return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _HKC:
|
|
27
|
+
def __init__(self, available=True): self.available = available; self.client = _Client()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _msg(payload): return types.SimpleNamespace(payload=payload)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_disabled_default():
|
|
34
|
+
print("\n【1】默认关:不结晶(行为不变)")
|
|
35
|
+
hkc = _HKC()
|
|
36
|
+
tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=False)
|
|
37
|
+
tc.on_truth(_msg({"truth": {"fact": "毕昇发明活字印刷术"}, "agent_id": "office"}))
|
|
38
|
+
tc.drain()
|
|
39
|
+
assert hkc.client.ingested == [], "默认关不应结晶"
|
|
40
|
+
tc.close()
|
|
41
|
+
print(" ✓ enabled=False → 零结晶")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_enabled_crystallizes():
|
|
45
|
+
print("\n【2】启用:后台经 ingest_text(KDE→KEE)结晶真值")
|
|
46
|
+
hkc = _HKC()
|
|
47
|
+
tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=True)
|
|
48
|
+
tc.on_truth(_msg({"truth": {"fact": "毕昇发明活字印刷术"}, "agent_id": "office"}))
|
|
49
|
+
tc.drain()
|
|
50
|
+
assert hkc.client.ingested == [("毕昇发明活字印刷术", "office")], hkc.client.ingested
|
|
51
|
+
tc.close()
|
|
52
|
+
print(f" ✓ 结晶: {hkc.client.ingested}(domain 取 agent_id)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_blocked_and_unavailable_skipped():
|
|
56
|
+
print("\n【3】被拦截 / HKC 不可用 → 跳过(不崩)")
|
|
57
|
+
hkc = _HKC()
|
|
58
|
+
tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=True)
|
|
59
|
+
tc.on_truth(_msg({"truth": {"fact": "x"}, "agent_id": "finance", "blocked": True}))
|
|
60
|
+
tc.drain()
|
|
61
|
+
assert hkc.client.ingested == [], "被拦截任务不结晶"
|
|
62
|
+
un = _HKC(available=False)
|
|
63
|
+
tc2 = TruthCrystallizer(hkc_provider=lambda: un, enabled=True)
|
|
64
|
+
tc2.on_truth(_msg({"truth": {"fact": "y"}, "agent_id": "finance"}))
|
|
65
|
+
tc2.drain()
|
|
66
|
+
assert un.client.ingested == [], "HKC 不可用则跳过"
|
|
67
|
+
tc.close(); tc2.close()
|
|
68
|
+
print(" ✓ blocked 跳过;HKC 不可用跳过、不崩")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_pending_bounded_no_leak():
|
|
72
|
+
print("\n【3b】深审回归:持续结晶下 _pending 不无界增长(prune 已完成 Future)")
|
|
73
|
+
hkc = _HKC()
|
|
74
|
+
tc = TruthCrystallizer(hkc_provider=lambda: hkc, enabled=True)
|
|
75
|
+
for i in range(200):
|
|
76
|
+
tc.on_truth(_msg({"truth": {"fact": f"事实{i}"}, "agent_id": "office"}))
|
|
77
|
+
tc.drain()
|
|
78
|
+
tc.on_truth(_msg({"truth": {"fact": "末尾"}, "agent_id": "office"}))
|
|
79
|
+
import time as _t
|
|
80
|
+
for _ in range(50):
|
|
81
|
+
if all(f.done() for f in tc._pending):
|
|
82
|
+
break
|
|
83
|
+
_t.sleep(0.02)
|
|
84
|
+
assert len(tc._pending) <= 2, f"_pending 应只留在飞行中的,实得 {len(tc._pending)}"
|
|
85
|
+
assert len(hkc.client.ingested) == 201
|
|
86
|
+
tc.close()
|
|
87
|
+
print(f" ✓ 提交 201 次,_pending 稳定在 {len(tc._pending)}(非 201)→ 无泄漏")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_integration_wiring():
|
|
91
|
+
print("\n【4】真实接线:Foxclaw 订阅 truth.appended → 触发结晶")
|
|
92
|
+
d = tempfile.mkdtemp(prefix="truthx_")
|
|
93
|
+
try:
|
|
94
|
+
from foxclaw import Foxclaw
|
|
95
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
96
|
+
fake = _HKC()
|
|
97
|
+
os_.truth_crystallizer._hkc_provider = lambda: fake
|
|
98
|
+
os_.truth_crystallizer.enabled = True
|
|
99
|
+
os_.system_bus.publish("truth.appended",
|
|
100
|
+
{"truth": {"fact": "测试真值事实"}, "agent_id": "finance"})
|
|
101
|
+
os_.truth_crystallizer.drain()
|
|
102
|
+
assert fake.client.ingested and fake.client.ingested[0][0] == "测试真值事实", \
|
|
103
|
+
f"应经订阅触发结晶,实得 {fake.client.ingested}"
|
|
104
|
+
print(f" ✓ 发 truth.appended → 结晶 {fake.client.ingested}")
|
|
105
|
+
asyncio.run(os_.aclose())
|
|
106
|
+
finally:
|
|
107
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
print("=" * 60)
|
|
112
|
+
print("真值走 HKC/KEE · TruthCrystallizer(v2 ④ Step1)")
|
|
113
|
+
print("=" * 60)
|
|
114
|
+
test_disabled_default()
|
|
115
|
+
test_enabled_crystallizes()
|
|
116
|
+
test_blocked_and_unavailable_skipped()
|
|
117
|
+
test_pending_bounded_no_leak()
|
|
118
|
+
test_integration_wiring()
|
|
119
|
+
print("\n" + "=" * 60)
|
|
120
|
+
print("✓ Step1 验证完成:默认关(双写不变) + 启用后台结晶 + 跳过降级 + 订阅接线")
|
|
121
|
+
print("=" * 60)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
真值流全路径触发(P2)测试 —— 全离线(stub LLM,bogus embedding 让 harness 走 hash)。
|
|
3
|
+
|
|
4
|
+
审计 P2 前提"删重复落库"已被代码推翻(TruthPipeline 写的是**有意的共享真值记录**,
|
|
5
|
+
非私有情景记忆的重复)。真正的问题是"共享真值记录只在主管分发路径产生"的不一致。
|
|
6
|
+
本次修复:统一模板 run_task 也发 execution.completed → 直连领域 Agent 的任务同样进真值链;
|
|
7
|
+
主管分发时子 Agent 带 _no_emit,由主管统一发(带 schedule),避免双发。
|
|
8
|
+
|
|
9
|
+
覆盖:
|
|
10
|
+
1. _no_emit 守卫:context['_no_emit']=True 时子 Agent 不自发 execution.completed
|
|
11
|
+
2. 全路径触发:直连领域 Agent 任务发**恰好一条** execution.completed
|
|
12
|
+
3. 共享真值记录:直连任务经 TruthPipeline 产生 shared=True 记录,别的 Agent 能召回
|
|
13
|
+
"""
|
|
14
|
+
import os, sys, asyncio, tempfile, shutil
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
17
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
18
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
print("=" * 60)
|
|
24
|
+
print("真值流 · 全路径触发(P2)")
|
|
25
|
+
print("=" * 60)
|
|
26
|
+
d = tempfile.mkdtemp(prefix="truth_")
|
|
27
|
+
try:
|
|
28
|
+
from foxclaw import Foxclaw
|
|
29
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
30
|
+
seen = []
|
|
31
|
+
os_.event_spine.project(lambda e: seen.append(e.topic))
|
|
32
|
+
|
|
33
|
+
print("\n【1】_no_emit 守卫:分发时子 Agent 不自发 execution.completed")
|
|
34
|
+
asyncio.run(os_.finance.run_task("守卫测试", {"_no_emit": True}))
|
|
35
|
+
assert "execution.completed" not in seen, "带 _no_emit 时不应发 execution.completed"
|
|
36
|
+
print(" ✓ context['_no_emit']=True → 子 Agent 未自发(留给主管统一发)")
|
|
37
|
+
|
|
38
|
+
print("\n【2】全路径触发:直连领域 Agent 任务发恰好一条 execution.completed")
|
|
39
|
+
before = seen.count("execution.completed")
|
|
40
|
+
asyncio.run(os_.finance.run_task("分析我的月度开支"))
|
|
41
|
+
emitted = seen.count("execution.completed") - before
|
|
42
|
+
assert emitted == 1, f"直连任务应发恰好 1 条,实得 {emitted}"
|
|
43
|
+
print(f" ✓ 直连 finance.run_task → execution.completed ×{emitted}")
|
|
44
|
+
|
|
45
|
+
print("\n【3】共享真值记录:直连任务经 TruthPipeline 产生 shared 记录,跨 Agent 可见")
|
|
46
|
+
shared = os_.hmr.recall(query="分析我的月度开支 处理", agent_id="office", top_k=10)
|
|
47
|
+
shared_hits = [m for m in shared if m.get("shared")]
|
|
48
|
+
assert shared_hits, "直连任务应产生 shared=True 真值记录(修复:此前仅分发路径有)"
|
|
49
|
+
print(f" ✓ office 召回到 {len(shared_hits)} 条共享真值记录(来自 finance 的直连任务)")
|
|
50
|
+
print(f" 样例: {shared_hits[0].get('content','')[:50]!r}")
|
|
51
|
+
|
|
52
|
+
print("\n【4】被拦截的直连任务:blocked=True + 不落共享记录 + 观测不崩")
|
|
53
|
+
import types
|
|
54
|
+
from foxclaw.agents.base import HarnessBlockedError
|
|
55
|
+
async def boom(self, task, ctx, sid, mem):
|
|
56
|
+
raise HarnessBlockedError(types.SimpleNamespace(reason="越界"), None)
|
|
57
|
+
os_.finance._execute = types.MethodType(boom, os_.finance)
|
|
58
|
+
blocked_flags = []
|
|
59
|
+
os_.event_spine.project(
|
|
60
|
+
lambda e: blocked_flags.append(e.payload.get("blocked"))
|
|
61
|
+
if e.topic == "execution.completed" else None)
|
|
62
|
+
b_before = len([x for x in blocked_flags if x is True])
|
|
63
|
+
r = asyncio.run(os_.finance.run_task("危险越界任务"))
|
|
64
|
+
b_emitted = [x for x in blocked_flags if x is True]
|
|
65
|
+
assert r.get("error") == "harness_blocked"
|
|
66
|
+
assert len(b_emitted) - b_before == 1, "被拦截直连应发恰好一条 blocked=True"
|
|
67
|
+
own = [m for m in os_.hmr.recall(query="危险越界任务 处理", agent_id="office", top_k=10)
|
|
68
|
+
if m.get("shared") and "危险越界" in m.get("content", "")]
|
|
69
|
+
assert len(own) == 0, "被拦截任务不应产生自己的共享真值记录(TruthPipeline 跳过 blocked)"
|
|
70
|
+
snap = os_.cognitive_state.snapshot()
|
|
71
|
+
assert "active_agent" in snap
|
|
72
|
+
print(f" ✓ blocked=True ×1、无共享记录、CognitiveState 快照正常(focus={snap.get('semantic_focus')})")
|
|
73
|
+
|
|
74
|
+
asyncio.run(os_.aclose())
|
|
75
|
+
finally:
|
|
76
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
77
|
+
|
|
78
|
+
print("\n" + "=" * 60)
|
|
79
|
+
print("✓ P2 验证完成:全路径触发 + _no_emit 防双发 + 共享真值记录一致")
|
|
80
|
+
print("=" * 60)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_truth_flow():
|
|
84
|
+
main()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""新版本检查:默认关、版本比较、缓存 TTL、失败静默。
|
|
2
|
+
|
|
3
|
+
全部离线:``_fetch_latest`` 一律用 monkeypatch 替换,不发真实网络请求。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from foxclaw.core import update_check as uc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _reset() -> None:
|
|
14
|
+
uc._inflight = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_default_disabled(monkeypatch, tmp_path):
|
|
18
|
+
"""不设 FOXCLAW_UPDATE_CHECK → 不查、不写缓存、不起线程。"""
|
|
19
|
+
_reset()
|
|
20
|
+
monkeypatch.delenv("FOXCLAW_UPDATE_CHECK", raising=False)
|
|
21
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
|
|
22
|
+
|
|
23
|
+
assert uc.status()["enabled"] is False
|
|
24
|
+
uc.maybe_refresh()
|
|
25
|
+
assert uc._inflight is False
|
|
26
|
+
assert not (tmp_path / "update_check.json").exists()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_version_compare():
|
|
30
|
+
assert uc.parse_version("0.1.10") == (0, 1, 10)
|
|
31
|
+
assert uc.parse_version("0.2.0rc1") == (0, 2, 0)
|
|
32
|
+
assert uc.parse_version("") == ()
|
|
33
|
+
assert uc.is_newer("0.1.10", "0.1.2") is True
|
|
34
|
+
assert uc.is_newer("0.1.2", "0.1.2") is False
|
|
35
|
+
assert uc.is_newer("0.1.2", "0.1.10") is False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_refresh_writes_cache(monkeypatch, tmp_path):
|
|
39
|
+
_reset()
|
|
40
|
+
monkeypatch.setenv("FOXCLAW_UPDATE_CHECK", "1")
|
|
41
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
|
|
42
|
+
monkeypatch.setattr(uc, "_fetch_latest", lambda: "9.9.9")
|
|
43
|
+
|
|
44
|
+
uc._refresh()
|
|
45
|
+
|
|
46
|
+
st = uc.status()
|
|
47
|
+
assert st["latest"] == "9.9.9"
|
|
48
|
+
assert st["update_available"] is True
|
|
49
|
+
assert st["error"] is None
|
|
50
|
+
assert (tmp_path / "update_check.json").exists()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_fresh_cache_skips_refresh(monkeypatch, tmp_path):
|
|
54
|
+
"""24h 内不重复查 PyPI。"""
|
|
55
|
+
_reset()
|
|
56
|
+
monkeypatch.setenv("FOXCLAW_UPDATE_CHECK", "1")
|
|
57
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
|
|
58
|
+
uc._write_cache({"latest": "9.9.9", "checked_at": time.time(), "current": "0.1.2"})
|
|
59
|
+
|
|
60
|
+
calls: list[int] = []
|
|
61
|
+
monkeypatch.setattr(uc, "_fetch_latest", lambda: calls.append(1) or "9.9.9")
|
|
62
|
+
uc.maybe_refresh()
|
|
63
|
+
time.sleep(0.05)
|
|
64
|
+
|
|
65
|
+
assert calls == []
|
|
66
|
+
assert uc._inflight is False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_failure_is_silent(monkeypatch, tmp_path):
|
|
70
|
+
"""网络失败不抛异常,只记 error;update_available 仍为 False。"""
|
|
71
|
+
_reset()
|
|
72
|
+
monkeypatch.setenv("FOXCLAW_UPDATE_CHECK", "1")
|
|
73
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
|
|
74
|
+
|
|
75
|
+
def boom():
|
|
76
|
+
raise OSError("offline")
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(uc, "_fetch_latest", boom)
|
|
79
|
+
uc._refresh()
|
|
80
|
+
|
|
81
|
+
st = uc.status()
|
|
82
|
+
assert st["update_available"] is False
|
|
83
|
+
assert "offline" in (st["error"] or "")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_disabled_never_reports_update(monkeypatch, tmp_path):
|
|
87
|
+
"""缓存里就算有更新的版本,没启用也不提示(默认不外呼,也不拿旧缓存骚扰)。"""
|
|
88
|
+
_reset()
|
|
89
|
+
monkeypatch.delenv("FOXCLAW_UPDATE_CHECK", raising=False)
|
|
90
|
+
monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
|
|
91
|
+
uc._write_cache({"latest": "99.0.0", "checked_at": time.time(), "current": "0.1.2"})
|
|
92
|
+
|
|
93
|
+
st = uc.status()
|
|
94
|
+
assert st["enabled"] is False
|
|
95
|
+
assert st["update_available"] is False
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""浏览器导航 URL 准入策略(SSRF / 本地文件读取防护)。
|
|
2
|
+
|
|
3
|
+
背景:`ActionExecutor._do_navigate` 原本把模型给的字符串直接交给 driver,
|
|
4
|
+
`file://` 可读本机文件、`javascript:` 可在页面上下文执行、内网地址可作探测跳板。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from foxclaw.browser.action.url_policy import validate_navigate_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("url", [
|
|
15
|
+
"file:///C:/Windows/win.ini",
|
|
16
|
+
"javascript:alert(1)",
|
|
17
|
+
"data:text/html,<script>alert(1)</script>",
|
|
18
|
+
"blob:https://example.com/uuid",
|
|
19
|
+
"ftp://example.com/x",
|
|
20
|
+
])
|
|
21
|
+
def test_rejects_non_http_schemes(url: str) -> None:
|
|
22
|
+
with pytest.raises(ValueError):
|
|
23
|
+
validate_navigate_url(url)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("url", [
|
|
27
|
+
"http://127.0.0.1:8000/api/ahs/run",
|
|
28
|
+
"http://localhost/admin",
|
|
29
|
+
"http://[::1]:9000/",
|
|
30
|
+
"http://169.254.169.254/latest/meta-data/",
|
|
31
|
+
"http://10.0.0.5/",
|
|
32
|
+
"http://192.168.1.1/",
|
|
33
|
+
])
|
|
34
|
+
def test_rejects_internal_targets(url: str) -> None:
|
|
35
|
+
with pytest.raises(ValueError):
|
|
36
|
+
validate_navigate_url(url)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_allows_public_url() -> None:
|
|
40
|
+
assert validate_navigate_url("https://example.com/a?b=1") == "https://example.com/a?b=1"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_allow_private_escape_hatch(monkeypatch) -> None:
|
|
44
|
+
assert validate_navigate_url("http://127.0.0.1:8000/", allow_private=True) \
|
|
45
|
+
== "http://127.0.0.1:8000/"
|
|
46
|
+
monkeypatch.setenv("FOXCLAW_BROWSER_ALLOW_PRIVATE", "1")
|
|
47
|
+
assert validate_navigate_url("http://192.168.1.10/") == "http://192.168.1.10/"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_rejects_empty_and_missing_host() -> None:
|
|
51
|
+
for bad in ("", " ", "http://"):
|
|
52
|
+
with pytest.raises(ValueError):
|
|
53
|
+
validate_navigate_url(bad)
|