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,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)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""三个薄点:失败策略表 / 执行后核验 / 执行证据。
|
|
2
|
+
|
|
3
|
+
锁住的行为(对应审核报告里「静默假成功」那一类):
|
|
4
|
+
① 失败文本能归类成明确类型,而不是"模型自己看着办";
|
|
5
|
+
② 只有**只读**工具会自动重试一次 —— 有副作用的动作重试等于重复扣款/重复发消息;
|
|
6
|
+
③ 每步都有核验结论:有凭据=verified,没后置条件=unverified,失败=failed,**空结果不算成功**;
|
|
7
|
+
④ 证据只从真实返回里抽 URL / 磁盘上存在的文件,认不出的路径不冒充证据;
|
|
8
|
+
⑤ 这些结论挂到会话槽并由 `_run_tool_calls` 记账,不上屏到用户正文;
|
|
9
|
+
⑥ 成功结果的回注文本一个字都不改(否则会污染既有工具轮行为)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import types
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from foxclaw.agents.mixins import tool_exec
|
|
19
|
+
from foxclaw.core import failure_policy as policy
|
|
20
|
+
from foxclaw.core import tools as core_tools
|
|
21
|
+
from foxclaw.core import verify as vf
|
|
22
|
+
|
|
23
|
+
TIMEOUT_TEXT = "(工具 web_search 超过 45s 未返回,已跳过以免拖住整轮)"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Rec:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.steps: list = []
|
|
29
|
+
|
|
30
|
+
def record_step(self, tid, step):
|
|
31
|
+
self.steps.append((tid, step))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _agent():
|
|
35
|
+
a = types.SimpleNamespace()
|
|
36
|
+
a._recovery = _Rec()
|
|
37
|
+
a._session_id = "sess_test"
|
|
38
|
+
return a
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── ① 失败策略表 ─────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize("text,kind", [
|
|
44
|
+
(TIMEOUT_TEXT, policy.TIMEOUT),
|
|
45
|
+
("(工具 shell 已被策略禁用)", policy.DENIED),
|
|
46
|
+
("(联网搜索未配置,已降级)", policy.NOT_CONFIGURED),
|
|
47
|
+
("(入参不合法:缺少必填 query)", policy.INVALID_ARGS),
|
|
48
|
+
("(联网搜索未返回结果)", policy.EMPTY),
|
|
49
|
+
("(工具 web_search 执行异常:boom)", policy.EXCEPTION),
|
|
50
|
+
("中国未来十年的产业趋势如何", policy.OK), # 正文含"未"不能误判
|
|
51
|
+
("", policy.UNKNOWN), # 空结果不能当成功
|
|
52
|
+
("一切正常", policy.OK),
|
|
53
|
+
])
|
|
54
|
+
def test_classify(text, kind) -> None:
|
|
55
|
+
assert policy.classify(text) == kind
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_retry_only_for_read_only() -> None:
|
|
59
|
+
read = policy.decide(TIMEOUT_TEXT, capability={"read_only": True}, attempt=1)
|
|
60
|
+
assert read.action == policy.RETRY
|
|
61
|
+
# 第二次还失败 → 不再重试,交人
|
|
62
|
+
assert policy.decide(TIMEOUT_TEXT, capability={"read_only": True}, attempt=2).action \
|
|
63
|
+
== policy.HAND_TO_HUMAN
|
|
64
|
+
# 有副作用:第一次就不许自动重试
|
|
65
|
+
write = policy.decide(TIMEOUT_TEXT, capability={"read_only": False}, attempt=1)
|
|
66
|
+
assert write.action == policy.HAND_TO_HUMAN
|
|
67
|
+
assert "重复执行" in write.hint
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_decide_never_optimistic() -> None:
|
|
71
|
+
assert policy.decide("(联网搜索未配置)").action == policy.DEGRADE
|
|
72
|
+
assert policy.decide("(工具 x 已被策略禁用)").action == policy.STOP
|
|
73
|
+
# 形如失败但聊不归类 → 保守交人,不乐观当成功
|
|
74
|
+
assert policy.decide("(看不懂的回注").kind == policy.UNKNOWN
|
|
75
|
+
assert policy.decide("(看不懂的回注").action == policy.HAND_TO_HUMAN
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_annotate_leaves_success_untouched() -> None:
|
|
79
|
+
d = policy.decide("搜索到的正文", capability={"read_only": True})
|
|
80
|
+
assert policy.annotate("搜索到的正文", d) == "搜索到的正文"
|
|
81
|
+
out = policy.annotate(TIMEOUT_TEXT, policy.decide(TIMEOUT_TEXT, capability={"read_only": True},
|
|
82
|
+
attempt=2))
|
|
83
|
+
assert out.startswith(TIMEOUT_TEXT)
|
|
84
|
+
assert "[执行策略]" in out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── ② 执行后核验 ─────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def test_failure_is_failed_not_verified() -> None:
|
|
90
|
+
v = vf.verify("web_search", {}, TIMEOUT_TEXT, {"read_only": True})
|
|
91
|
+
assert v.status == vf.FAILED
|
|
92
|
+
assert v.reason.startswith("失败类型")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_empty_result_is_not_success() -> None:
|
|
96
|
+
v = vf.verify("web_search", {}, "", {"read_only": True})
|
|
97
|
+
assert v.status == vf.FAILED
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_read_tool_with_payload_is_verified(tmp_path) -> None:
|
|
101
|
+
p = tmp_path / "report.md"
|
|
102
|
+
p.write_text("x", encoding="utf-8")
|
|
103
|
+
v = vf.verify("read_file", {}, f"见 {p}", {"read_only": True})
|
|
104
|
+
assert v.status == vf.VERIFIED
|
|
105
|
+
assert [e.kind for e in v.evidence] == ["file"]
|
|
106
|
+
assert v.evidence[0].ref == str(p)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_side_effect_without_postcondition_is_unverified() -> None:
|
|
110
|
+
v = vf.verify("send_mail", {}, "已发送", {"read_only": False})
|
|
111
|
+
assert v.status == vf.UNVERIFIED
|
|
112
|
+
assert "后置条件" in v.reason
|
|
113
|
+
# 只读工具拿到内容就算这一步发生过(内容可信度归污点层)
|
|
114
|
+
assert vf.verify("recall_memory", {}, "【召回的相关记忆】\n- a", {"read_only": True}).status \
|
|
115
|
+
== vf.VERIFIED
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_web_search_without_source_is_unverified() -> None:
|
|
119
|
+
v = vf.verify("web_search", {}, "【联网搜索结果】据行业报告……", {"read_only": True})
|
|
120
|
+
assert v.status == vf.UNVERIFIED
|
|
121
|
+
assert "来源" in v.reason
|
|
122
|
+
v2 = vf.verify("web_search", {}, "【联网搜索结果】见 https://example.com/a", {"read_only": True})
|
|
123
|
+
assert v2.status == vf.VERIFIED
|
|
124
|
+
assert v2.evidence[0].label == "example.com"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_evidence_only_counts_real_paths(tmp_path) -> None:
|
|
128
|
+
ghost = "C:/definitely/not/here/x.md"
|
|
129
|
+
assert vf.extract_evidence("read_file", f"读了 {ghost}") == []
|
|
130
|
+
real = tmp_path / "a.md"
|
|
131
|
+
real.write_text("x", encoding="utf-8")
|
|
132
|
+
ev = vf.extract_evidence("read_file", f"读了 {real}")
|
|
133
|
+
assert [e.ref for e in ev] == [str(real)]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_evidence_dedup_and_cap() -> None:
|
|
137
|
+
text = " ".join([f"https://example.com/{i}" for i in range(20)]) + " https://example.com/1"
|
|
138
|
+
ev = vf.extract_evidence("web_search", text)
|
|
139
|
+
assert len(ev) == vf.MAX_EVIDENCE
|
|
140
|
+
assert len({e.ref for e in ev}) == vf.MAX_EVIDENCE
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_note_for_model() -> None:
|
|
144
|
+
ok = vf.Verification(vf.VERIFIED)
|
|
145
|
+
assert vf.note_for_model(ok) == ""
|
|
146
|
+
un_read = vf.Verification(vf.UNVERIFIED, "无来源")
|
|
147
|
+
assert vf.note_for_model(un_read, read_only=True) == "" # 只读不训话,免得污染上下文
|
|
148
|
+
assert "不要断言" in vf.note_for_model(un_read, read_only=False)
|
|
149
|
+
bad = vf.Verification(vf.FAILED, "失败类型:timeout")
|
|
150
|
+
assert "不要向用户声称" in vf.note_for_model(bad)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── ③ 接进工具轮 ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
def test_retries_read_only_once_then_gives_up(monkeypatch) -> None:
|
|
156
|
+
agent = _agent()
|
|
157
|
+
batches: list = []
|
|
158
|
+
|
|
159
|
+
def fake_batch(a, calls):
|
|
160
|
+
batches.append([c["name"] for c in calls])
|
|
161
|
+
return [TIMEOUT_TEXT] * len(calls)
|
|
162
|
+
|
|
163
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch", fake_batch)
|
|
164
|
+
monkeypatch.setattr(core_tools, "tool_capability", lambda n: {"read_only": True})
|
|
165
|
+
out = tool_exec._run_tool_calls(agent, [{"name": "web_search", "args": {"q": "x"}}])
|
|
166
|
+
|
|
167
|
+
assert len(batches) == 2 # 第一次 + 自动重试一次
|
|
168
|
+
assert "[执行策略]" in out[0] and "不要" in out[0]
|
|
169
|
+
assert agent._recovery.steps == [] # 失败的步不许存进断点
|
|
170
|
+
ev = agent._evidence_list
|
|
171
|
+
assert ev and ev[0]["status"] == vf.FAILED
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_side_effect_failure_is_not_retried(monkeypatch) -> None:
|
|
175
|
+
agent = _agent()
|
|
176
|
+
batches: list = []
|
|
177
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch",
|
|
178
|
+
lambda a, calls: (batches.append(len(calls)), [TIMEOUT_TEXT] * len(calls))[1])
|
|
179
|
+
monkeypatch.setattr(core_tools, "tool_capability", lambda n: {"read_only": False})
|
|
180
|
+
out = tool_exec._run_tool_calls(agent, [{"name": "send_mail", "args": {}}])
|
|
181
|
+
assert batches == [1] # 一次,不重试
|
|
182
|
+
assert "重复执行" in out[0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_success_text_untouched_and_evidence_recorded(monkeypatch) -> None:
|
|
186
|
+
agent = _agent()
|
|
187
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch",
|
|
188
|
+
lambda a, calls: ["【联网搜索结果】结论 https://example.com/a"] * len(calls))
|
|
189
|
+
monkeypatch.setattr(core_tools, "tool_capability", lambda n: {"read_only": True})
|
|
190
|
+
out = tool_exec._run_tool_calls(agent, [{"name": "web_search", "args": {"q": "x"}}])
|
|
191
|
+
|
|
192
|
+
assert out[0] == "【联网搜索结果】结论 https://example.com/a" # 成功文本不被改
|
|
193
|
+
assert len(agent._recovery.steps) == 1 # 成功的步可复用
|
|
194
|
+
ev = agent._evidence_list[0]
|
|
195
|
+
assert ev["status"] == vf.VERIFIED
|
|
196
|
+
assert ev["evidence"][0]["ref"] == "https://example.com/a"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_unverified_side_effect_tells_model_not_to_claim_done(monkeypatch) -> None:
|
|
200
|
+
agent = _agent()
|
|
201
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch", lambda a, calls: ["已发送"] * len(calls))
|
|
202
|
+
monkeypatch.setattr(core_tools, "tool_capability", lambda n: {"read_only": False})
|
|
203
|
+
out = tool_exec._run_tool_calls(agent, [{"name": "send_mail", "args": {}}])
|
|
204
|
+
assert out[0].startswith("已发送")
|
|
205
|
+
assert "不要断言" in out[0]
|
|
206
|
+
assert agent._evidence_list[0]["status"] == vf.UNVERIFIED
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── ④ 证据不串台 ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
def test_evidence_is_per_agent() -> None:
|
|
212
|
+
from foxclaw.agents.mixins import _context
|
|
213
|
+
|
|
214
|
+
a, b = _agent(), _agent()
|
|
215
|
+
_context._evidence_reset(a)
|
|
216
|
+
_context._evidence_reset(b)
|
|
217
|
+
_context._evidence_add(a, {"tool": "x", "status": "verified"})
|
|
218
|
+
assert _context._evidence_take(a) and not _context._evidence_take(b)
|
|
219
|
+
# take 之后清空:下一轮不会重复上报上一轮的证据
|
|
220
|
+
assert _context._evidence_take(a) == []
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_verify_never_raises_on_weird_input() -> None:
|
|
224
|
+
assert vf.verify("web_search", {}, "(((", None).status == vf.FAILED
|
|
225
|
+
assert vf.verify("unknown_tool", None, "ok", None).status == vf.UNVERIFIED
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""版本号单一来源校验。
|
|
2
|
+
|
|
3
|
+
版本号目前硬编码在四个地方,任何一处漏改都会造成「本地是 0.2.0、npm 上
|
|
4
|
+
还是 0.1.0」这类对外不一致(`npm/index.js` 的 version 是发布物里用户
|
|
5
|
+
`import` 直接读到的值,最容易漏):
|
|
6
|
+
|
|
7
|
+
- ``pyproject.toml`` 的 ``project.version`` —— PyPI 包版本
|
|
8
|
+
- ``foxclaw/__init__.py`` 的 ``__version__`` —— 运行时 ``foxclaw --version``
|
|
9
|
+
- ``npm/package.json`` 的 ``version`` —— npm 包版本
|
|
10
|
+
- ``npm/index.js`` 的 ``export const version`` —— npm 包对外导出的版本
|
|
11
|
+
|
|
12
|
+
本测试只做「四处必须相等」,不校验具体数值,发版时改哪一处都不用动它。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import tomllib
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from foxclaw import __version__
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
NPM_DIR = REPO_ROOT / "npm"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _project_version() -> str:
|
|
29
|
+
with (REPO_ROOT / "pyproject.toml").open("rb") as fh:
|
|
30
|
+
return tomllib.load(fh)["project"]["version"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _project_name() -> str:
|
|
34
|
+
with (REPO_ROOT / "pyproject.toml").open("rb") as fh:
|
|
35
|
+
return tomllib.load(fh)["project"]["name"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _npm_package() -> dict:
|
|
39
|
+
return json.loads((NPM_DIR / "package.json").read_text(encoding="utf-8"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_python_versions_agree() -> None:
|
|
43
|
+
"""pyproject 与运行时 __version__ 一致。"""
|
|
44
|
+
assert __version__ == _project_version()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_npm_package_version_agrees() -> None:
|
|
48
|
+
"""npm/package.json 与 PyPI 侧版本一致。"""
|
|
49
|
+
assert _npm_package()["version"] == _project_version()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_npm_index_export_version_agrees() -> None:
|
|
53
|
+
"""npm/index.js 导出值(发布物对外可见)与 package.json 一致。"""
|
|
54
|
+
source = (NPM_DIR / "index.js").read_text(encoding="utf-8")
|
|
55
|
+
match = re.search(r'export\s+const\s+version\s*=\s*["\']([^"\']+)["\']', source)
|
|
56
|
+
assert match, "npm/index.js 里找不到 `export const version = \"...\"`"
|
|
57
|
+
assert match.group(1) == _npm_package()["version"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_package_names_are_foxclaw() -> None:
|
|
61
|
+
"""PyPI 与 npm 两侧包名必须同名,且为 foxclaw。"""
|
|
62
|
+
assert _project_name() == "foxclaw"
|
|
63
|
+
assert _npm_package()["name"] == _project_name()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_npm_index_export_name_agrees() -> None:
|
|
67
|
+
"""npm/index.js 导出的 name 与 package.json 一致。"""
|
|
68
|
+
source = (NPM_DIR / "index.js").read_text(encoding="utf-8")
|
|
69
|
+
match = re.search(r'export\s+const\s+name\s*=\s*["\']([^"\']+)["\']', source)
|
|
70
|
+
assert match, "npm/index.js 里找不到 `export const name = \"...\"`"
|
|
71
|
+
assert match.group(1) == _npm_package()["name"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""联网触发判定:个人数据问题绝不联网,外部世界问题正常联网。
|
|
2
|
+
|
|
3
|
+
背景(链路追踪抓出来的真 bug):原先纯时间词(今天/本月/最近…)也算触发,而 Foxclaw 的
|
|
4
|
+
领域(个人财务/行程/学习)天生满是时间词 → 「分析一下本月支出」这类**问自己账本**的任务
|
|
5
|
+
被误判为需联网,白跑 ~22s 的付费深度搜索。错搜代价远大于漏搜,故改为:
|
|
6
|
+
个人数据否决 > 外部话题触发 > 纯时间词不触发。
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
def test_web_trigger():
|
|
14
|
+
# ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
|
|
15
|
+
# 假红。全量请用 tests/run_all.py(每文件独立子进程)。
|
|
16
|
+
if "foxclaw.core.web_search" in sys.modules:
|
|
17
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
18
|
+
from foxclaw.core.web_search import (needs_search, TOPIC_TRIGGERS,
|
|
19
|
+
TIME_WORDS)
|
|
20
|
+
|
|
21
|
+
print("=" * 60); print("联网触发判定验证"); print("=" * 60)
|
|
22
|
+
|
|
23
|
+
# [1] 个人数据 → 绝不联网(答案在本地领域内核里,不在网上)
|
|
24
|
+
personal = ["分析一下本月支出", "本月餐饮花了多少", "今天的行程安排",
|
|
25
|
+
"本周要复习什么知识点", "账户余额还剩多少", "这个月消费记录"]
|
|
26
|
+
for q in personal:
|
|
27
|
+
assert needs_search(q) is False, f"个人数据不该联网: {q}"
|
|
28
|
+
print(f"[1] 个人数据不联网({len(personal)} 例,含「本月支出」这类原 bug 用例)OK")
|
|
29
|
+
|
|
30
|
+
# [2] 个人数据 + 话题词 → 仍不联网(否决优先级最高)
|
|
31
|
+
# 「本月支出多少钱」含话题词「多少钱」,但问的是自己的账 → 必须否决
|
|
32
|
+
assert needs_search("本月支出多少钱") is False, "个人数据否决应压过话题触发"
|
|
33
|
+
assert needs_search("我的预算还剩多少钱") is False
|
|
34
|
+
print("[2] 个人数据否决压过话题触发(「本月支出多少钱」不联网)OK")
|
|
35
|
+
|
|
36
|
+
# [3] 外部世界话题 → 正常联网(不能因为修 bug 把真需求也砍了)
|
|
37
|
+
external = ["今天天气怎么样", "最新的AI新闻", "iPhone现在多少钱",
|
|
38
|
+
"特斯拉股价", "python最新版本", "美元汇率"]
|
|
39
|
+
for q in external:
|
|
40
|
+
assert needs_search(q) is True, f"外部话题应联网: {q}"
|
|
41
|
+
print(f"[3] 外部话题正常联网({len(external)} 例)OK")
|
|
42
|
+
|
|
43
|
+
# [4] 「我的」不算个人标记 —— 「我的股票现在多少钱」是真要联网的
|
|
44
|
+
assert needs_search("我的股票现在多少钱") is True, "「我的」过宽会误杀真实联网需求"
|
|
45
|
+
print("[4] 「我的股票现在多少钱」仍联网(个人标记不能放「我的」这类过宽词)OK")
|
|
46
|
+
|
|
47
|
+
# [5] 纯时间词不触发(本 bug 的核心)
|
|
48
|
+
for q in ["现在几点", "最近怎么样", "今天", "本月"]:
|
|
49
|
+
assert needs_search(q) is False, f"纯时间词不该单独触发: {q}"
|
|
50
|
+
assert not (set(TIME_WORDS) & set(TOPIC_TRIGGERS)), "时间词不得混入话题触发表(否则 bug 复发)"
|
|
51
|
+
print(f"[5] 纯时间词不单独触发({len(TIME_WORDS)} 个时间词与话题表零交集)OK")
|
|
52
|
+
|
|
53
|
+
# [6] 闲聊/编码 → 不联网
|
|
54
|
+
for q in ["你好", "帮我写个函数", ""]:
|
|
55
|
+
assert needs_search(q) is False, q
|
|
56
|
+
print("[6] 闲聊/编码/空串 不联网 OK")
|
|
57
|
+
|
|
58
|
+
# [7] 英文词边界仍生效(旧修复不能退化:now⊄knowledge, current⊄concurrent)
|
|
59
|
+
assert needs_search("knowledge base") is False, "now 不该子串命中 knowledge"
|
|
60
|
+
assert needs_search("concurrent tasks") is False, "current 不该子串命中 concurrent"
|
|
61
|
+
assert needs_search("latest news") is True
|
|
62
|
+
print("[7] 英文词边界不退化(knowledge/concurrent 不误命中)OK")
|
|
63
|
+
|
|
64
|
+
# [8] 【防复发·跨路径】个人数据保护必须是**单一真源**,所有"要不要发到外部"的路径都得遵守。
|
|
65
|
+
# 真实教训:这条保护原先只写在 needs_search 里;技能派发默认开启后,
|
|
66
|
+
# 「查一下最新的本月支出」经**技能路由**照样把私人财务问题发去 external 的 web_search
|
|
67
|
+
# —— 同一个 bug 从没设防的旁路回来了,还多了数据外泄。
|
|
68
|
+
from foxclaw.core.web_search import is_personal_query
|
|
69
|
+
from foxclaw.pipeline import stage_route
|
|
70
|
+
from foxclaw.pipeline.cognitive_pipeline import PipelineContext
|
|
71
|
+
from foxclaw.skills import build_default_registry
|
|
72
|
+
|
|
73
|
+
assert is_personal_query("查一下最新的本月支出") is True
|
|
74
|
+
assert is_personal_query("查一下最新新闻") is False
|
|
75
|
+
|
|
76
|
+
_reg = build_default_registry()
|
|
77
|
+
class _A: pass
|
|
78
|
+
def _route_target(task):
|
|
79
|
+
a = _A(); a.skill_registry = _reg
|
|
80
|
+
ctx = PipelineContext(agent=a, task=task)
|
|
81
|
+
stage_route(ctx)
|
|
82
|
+
return ctx.route["target"], ctx.route.get("skill_id")
|
|
83
|
+
|
|
84
|
+
# 私人数据 + 足够高的置信 → 仍不得路由到 **external** 技能(否则就是外泄)
|
|
85
|
+
for q in ["查一下最新的本月支出", "查一下最新的预算结余", "今天的行程 最新 查一下"]:
|
|
86
|
+
t, sid = _route_target(q)
|
|
87
|
+
assert t == "self", f"私人数据不得自动发给外部技能: {q} → {t}/{sid}"
|
|
88
|
+
# 但真实的外部问题照常走技能(不能因为修 bug 把功能砍了)
|
|
89
|
+
t, sid = _route_target("查一下最新新闻")
|
|
90
|
+
assert (t, sid) == ("skill", "web_search"), (t, sid)
|
|
91
|
+
# 本地只读技能不受影响
|
|
92
|
+
t, sid = _route_target("帮我计算 (12+8)*5")
|
|
93
|
+
assert (t, sid) == ("skill", "calculator"), (t, sid)
|
|
94
|
+
print("[8] 跨路径防复发:技能路由也遵守个人数据保护(不外泄)+ 不误伤真实外部需求 OK")
|
|
95
|
+
|
|
96
|
+
print("\n联网触发判定验证全部通过")
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
test_web_trigger()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""检查磁盘 faiss 索引的类型和 ntotal(不依赖服务进程)"""
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# 默认路径:仓库根下的运行数据目录,可用第一个命令行参数覆盖
|
|
7
|
+
faiss_path = str(Path(__file__).resolve().parents[1] / "foxclaw_data" / "hkc" / "search_vector.faiss")
|
|
8
|
+
if len(sys.argv) > 1:
|
|
9
|
+
faiss_path = sys.argv[1]
|
|
10
|
+
|
|
11
|
+
if not os.path.exists(faiss_path):
|
|
12
|
+
print(f"NOT_FOUND: {faiss_path}")
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import faiss
|
|
17
|
+
except ImportError:
|
|
18
|
+
print("faiss-cpu not installed (try: pip install faiss-cpu)")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
print(f"path: {faiss_path}")
|
|
22
|
+
print(f"size: {os.path.getsize(faiss_path)} bytes")
|
|
23
|
+
|
|
24
|
+
idx = faiss.read_index(faiss_path)
|
|
25
|
+
print(f"type : {type(idx).__name__}")
|
|
26
|
+
print(f"ntotal: {idx.ntotal}")
|
|
27
|
+
print(f"dim : {idx.d}")
|
|
28
|
+
if hasattr(idx, "nlist"):
|
|
29
|
+
print(f"nlist: {idx.nlist}")
|
|
30
|
+
if hasattr(idx, "metric_type"):
|
|
31
|
+
print(f"metric: {idx.metric_type}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# tools/check_log.ps1
|
|
2
|
+
# 检查 uvicorn 日志中是否有 IVF 升级关键字(行扫描 + FileShare.ReadWrite 兼容锁文件)。
|
|
3
|
+
# 用法:
|
|
4
|
+
# powershell -NoProfile -File tools\check_log.ps1 # 查 stdout(uvicorn.log)
|
|
5
|
+
# powershell -NoProfile -File tools\check_log.ps1 err # 查 stderr(uvicorn.err.log)
|
|
6
|
+
# powershell -NoProfile -File tools\check_log.ps1 err grep # 仅 grep 'VectorIndex 升级' 行
|
|
7
|
+
param(
|
|
8
|
+
[ValidateSet("stdout","err")] $which = "stdout",
|
|
9
|
+
[switch] $grep
|
|
10
|
+
)
|
|
11
|
+
$ErrorActionPreference = "Continue"
|
|
12
|
+
|
|
13
|
+
$logDir = Join-Path (Split-Path -Parent $PSScriptRoot) "foxclaw_data"
|
|
14
|
+
$log = if ($which -eq "err") { "$logDir\uvicorn.err.log" } else { "$logDir\uvicorn.log" }
|
|
15
|
+
|
|
16
|
+
if (-not (Test-Path $log)) {
|
|
17
|
+
Write-Host "[NOT FOUND] $log"
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# 用 [System.IO.File]::Open + FileShare.ReadWrite 兼容 python 文件正在写入的日志
|
|
22
|
+
# (PowerShell 5.1 Get-Content 在 UTF-8 + 文件被锁场景会返空,见 review [2])
|
|
23
|
+
$fs = [System.IO.File]::Open($log, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
24
|
+
$reader = New-Object System.IO.StreamReader($fs, [System.Text.Encoding]::UTF8)
|
|
25
|
+
$content = $reader.ReadToEnd()
|
|
26
|
+
$reader.Close()
|
|
27
|
+
$fs.Close()
|
|
28
|
+
|
|
29
|
+
if ($grep) {
|
|
30
|
+
foreach ($line in ($content -split "`n")) {
|
|
31
|
+
if ($line -match "VectorIndex 升级|VectorIndex 升级失败") {
|
|
32
|
+
Write-Host $line
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Write-Host "log file: $log (size=$($content.Length))"
|
|
39
|
+
Write-Host ("has_VectorIndex=" + $content.Contains("VectorIndex"))
|
|
40
|
+
Write-Host ("has_IVF=" + $content.Contains("IVF"))
|
|
41
|
+
Write-Host ("has_upgrade_zh=" + $content.Contains("升级"))
|
|
42
|
+
Write-Host ("has_IndexFlat=" + $content.Contains("IndexFlat"))
|
|
43
|
+
Write-Host ("has_IndexIVF=" + $content.Contains("IndexIVF"))
|
|
44
|
+
Write-Host "--- last 1500 chars ---"
|
|
45
|
+
$start = [Math]::Max(0, $content.Length - 1500)
|
|
46
|
+
Write-Host $content.Substring($start)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""检查 vector 索引状态 + 距离自动升级还差多少。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
python tools/ivf_status.py
|
|
5
|
+
|
|
6
|
+
输出:
|
|
7
|
+
vector_size, bm25_size, index_state, pending_ops
|
|
8
|
+
距 HKC_VECTOR_IVF_THRESHOLD 阈值还差多少 KU
|
|
9
|
+
下次升级触发时日志关键词
|
|
10
|
+
|
|
11
|
+
可调环境变量:
|
|
12
|
+
HKC_VECTOR_IVF_THRESHOLD 默认 10000(对应 .env 里的设定)
|
|
13
|
+
HKC_STATS_URL 默认 http://127.0.0.1:8000/stats
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
THRESHOLD = int(os.environ.get("HKC_VECTOR_IVF_THRESHOLD", "10000"))
|
|
23
|
+
STATS_URL = os.environ.get("HKC_STATS_URL", "http://127.0.0.1:8000/stats")
|
|
24
|
+
UPGRADE_KEYWORD = "VectorIndex 升级: Flat → IVF"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fetch_stats(url: str) -> dict:
|
|
28
|
+
with urllib.request.urlopen(url, timeout=5) as r:
|
|
29
|
+
return json.loads(r.read())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> int:
|
|
33
|
+
try:
|
|
34
|
+
s = fetch_stats(STATS_URL)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"[ERROR] 拉 {STATS_URL} 失败: {e}")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
vs = s.get("vector_size", 0)
|
|
40
|
+
bs = s.get("bm25_size", 0)
|
|
41
|
+
ib = s.get("index_build", {}) or {}
|
|
42
|
+
state = ib.get("index_state", "?")
|
|
43
|
+
pending = s.get("pending_ops", 0)
|
|
44
|
+
task_id = ib.get("task_id", "-")
|
|
45
|
+
|
|
46
|
+
remaining = max(0, THRESHOLD - vs)
|
|
47
|
+
pct = min(100.0, vs / THRESHOLD * 100) if THRESHOLD > 0 else 100.0
|
|
48
|
+
|
|
49
|
+
print("=" * 50)
|
|
50
|
+
print(f"vector_size : {vs}")
|
|
51
|
+
print(f"bm25_size : {bs}")
|
|
52
|
+
print(f"index_state : {state} task_id={task_id}")
|
|
53
|
+
print(f"pending_ops : {pending}")
|
|
54
|
+
print(f"threshold : {THRESHOLD}")
|
|
55
|
+
print(f"progress : {pct:5.1f}% (距升级还差 {remaining} KU)")
|
|
56
|
+
print("=" * 50)
|
|
57
|
+
|
|
58
|
+
if THRESHOLD <= 0:
|
|
59
|
+
print("阶段: Flat (升级禁用,env HKC_VECTOR_IVF_THRESHOLD=0)")
|
|
60
|
+
elif vs < THRESHOLD:
|
|
61
|
+
print(f"阶段: Flat (IndexFlatIP, O(N) 暴力精确)")
|
|
62
|
+
print(f"下次 vector_size 跨过 {THRESHOLD} 时 append_ku 会触发升级")
|
|
63
|
+
print(f"触发后看日志: grep '{UPGRADE_KEYWORD}' <foxclaw.log>")
|
|
64
|
+
else:
|
|
65
|
+
print(f"阶段: IVF (IndexIVFFlat, O(√N) 倒排)")
|
|
66
|
+
print(f"升级日志关键词: '{UPGRADE_KEYWORD}'")
|
|
67
|
+
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
sys.exit(main())
|