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,96 @@
|
|
|
1
|
+
"""app 配置与挂载:CORS、HKC 星球数据面、前端静态文件。
|
|
2
|
+
|
|
3
|
+
从 `foxclaw/api/__init__.py` 搬移出来的「挂载类」逻辑,均为接收 `app` 参数的纯函数,
|
|
4
|
+
在 `__init__.py` 创建 `app` 之后按原顺序调用,保持行为不变。
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import HTTPException
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
from fastapi.responses import FileResponse
|
|
13
|
+
|
|
14
|
+
import foxclaw.api as _api
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _configure_cors(app):
|
|
18
|
+
_cors_env = os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip()
|
|
19
|
+
if _cors_env == "*":
|
|
20
|
+
_CORS_ORIGINS = ["*"]
|
|
21
|
+
_CORS_ORIGIN_REGEX = None
|
|
22
|
+
elif _cors_env:
|
|
23
|
+
_CORS_ORIGINS = [o.strip() for o in _cors_env.split(",") if o.strip()]
|
|
24
|
+
_CORS_ORIGIN_REGEX = None
|
|
25
|
+
else:
|
|
26
|
+
_CORS_ORIGINS = []
|
|
27
|
+
_CORS_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$"
|
|
28
|
+
|
|
29
|
+
app.add_middleware(
|
|
30
|
+
CORSMiddleware,
|
|
31
|
+
allow_origins=_CORS_ORIGINS,
|
|
32
|
+
allow_origin_regex=_CORS_ORIGIN_REGEX,
|
|
33
|
+
allow_methods=["*"],
|
|
34
|
+
allow_headers=["*"],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _mount_hkc(app):
|
|
39
|
+
try:
|
|
40
|
+
from foxclaw.knowledge import _ensure_hkc_importable as _ensure_hkc
|
|
41
|
+
_ensure_hkc()
|
|
42
|
+
from hkc_api.routes.knowledge import router as _hkc_knowledge_router
|
|
43
|
+
from hkc_api.container import get_container as _hkc_get_container
|
|
44
|
+
|
|
45
|
+
def _resolve_hkc_container():
|
|
46
|
+
os_inst = _api.current_os()
|
|
47
|
+
svc = os_inst.hkc if os_inst is not None else None
|
|
48
|
+
if svc is None or not getattr(svc, "available", False):
|
|
49
|
+
raise HTTPException(503, "HKC 未启用(未 vendor 或缺依赖)")
|
|
50
|
+
return svc.container
|
|
51
|
+
|
|
52
|
+
app.dependency_overrides[_hkc_get_container] = _resolve_hkc_container
|
|
53
|
+
app.include_router(_hkc_knowledge_router)
|
|
54
|
+
@app.get("/stats", tags=["system"])
|
|
55
|
+
def _hkc_stats():
|
|
56
|
+
return _resolve_hkc_container().stats()
|
|
57
|
+
from hkc_api.routes.config import router as _hkc_config_router
|
|
58
|
+
app.include_router(_hkc_config_router)
|
|
59
|
+
try:
|
|
60
|
+
from hkc_api.routes.ability import router as _hkc_ability_router
|
|
61
|
+
from hkc_api.routes.search import router as _hkc_search_router
|
|
62
|
+
app.include_router(_hkc_ability_router)
|
|
63
|
+
app.include_router(_hkc_search_router)
|
|
64
|
+
_sub = "/abilities, /conflicts, /search/*"
|
|
65
|
+
except Exception as _e2:
|
|
66
|
+
_sub = f"(能力/搜索路由未挂: {_e2})"
|
|
67
|
+
print(f"[Foxclaw API] ✓ HKC 星球数据面已挂载 (/knowledge/*, /config/*, {_sub})")
|
|
68
|
+
except Exception as _e:
|
|
69
|
+
print(f"[Foxclaw API] · HKC 星球数据面未挂载(可选): {_e}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _mount_frontend(app):
|
|
73
|
+
frontend_dir = Path(__file__).parent.parent.parent / "frontend"
|
|
74
|
+
if not frontend_dir.exists():
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
v3_index = frontend_dir / "v3" / "index.html"
|
|
78
|
+
|
|
79
|
+
@app.get("/")
|
|
80
|
+
async def serve_root():
|
|
81
|
+
"""主界面。v3 未构建(没跑 npm run build)时退回旧 deck_v2,不让根路径 404。"""
|
|
82
|
+
if v3_index.exists():
|
|
83
|
+
return FileResponse(str(v3_index))
|
|
84
|
+
legacy = frontend_dir / "deck_v2.html"
|
|
85
|
+
return FileResponse(str(legacy if legacy.exists() else frontend_dir / "deck.html"))
|
|
86
|
+
|
|
87
|
+
@app.get("/classic")
|
|
88
|
+
async def serve_classic():
|
|
89
|
+
"""旧界面(deck_v2,v5.34):新版出问题时的手动回退入口,也留着它的二级面板历史。"""
|
|
90
|
+
return FileResponse(str(frontend_dir / "deck_v2.html"))
|
|
91
|
+
|
|
92
|
+
@app.get("/old")
|
|
93
|
+
async def serve_deck_old():
|
|
94
|
+
return FileResponse(str(frontend_dir / "deck.html"))
|
|
95
|
+
|
|
96
|
+
app.mount("/", StaticFiles(directory=str(frontend_dir), html=True), name="frontend")
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""API 中间件层:鉴权 / 统一网关限流 / 链路追踪 / 指标埋点。
|
|
2
|
+
|
|
3
|
+
从 foxclaw/api.py 原样搬移(纯搬移,零行为变更)。原先用 `@app.middleware("http")`
|
|
4
|
+
装饰器在 api 模块内直接注册;现集中到 `install_middleware(app)`,按**原注册顺序**
|
|
5
|
+
把 4 个 http 中间件挂到 app(注册顺序即执行顺序,保持不变)。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from ..core.gateway import Gateway
|
|
11
|
+
from ..core import settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _constant_time_eq(a: str, b: str) -> bool:
|
|
15
|
+
"""【P3-1】常量时间字符串比较,防计时攻击。
|
|
16
|
+
|
|
17
|
+
攻击者可通过普通 == 的逐字符短路耗时差异逐位猜出 API Key(猜对前缀越长、
|
|
18
|
+
响应越慢),从而把请求从 ip:* 身份伪造成 k:* 身份绕过 IP 限流。
|
|
19
|
+
compare_digest 接受 bytes 或 ASCII str;API Key 可能含非 ASCII,故统一先
|
|
20
|
+
encode("utf-8")。None 由调用方用 or "" 兜底。
|
|
21
|
+
"""
|
|
22
|
+
import hmac
|
|
23
|
+
return hmac.compare_digest((a or "").encode("utf-8"), (b or "").encode("utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_loopback_client(host: str) -> bool:
|
|
27
|
+
"""【P3-loopback】本机回环判定:127.0.0.0/8(RFC 3330) + ::1(RFC 4291)。
|
|
28
|
+
|
|
29
|
+
FastAPI/Starlette 的 `request.client.host` 已是 TCP 层解析后的远端 IP,
|
|
30
|
+
不识别 'localhost' 字符串(那是浏览器层主机名,握手时已解析为 IP)。
|
|
31
|
+
空值 / 解析失败 → False(默认走鉴权,偏安全)。
|
|
32
|
+
"""
|
|
33
|
+
if not host:
|
|
34
|
+
return False
|
|
35
|
+
h = host.strip()
|
|
36
|
+
if h == "::1":
|
|
37
|
+
return True
|
|
38
|
+
if h.startswith("127."):
|
|
39
|
+
parts = h.split(".")
|
|
40
|
+
if len(parts) == 4:
|
|
41
|
+
try:
|
|
42
|
+
if all(0 <= int(p) <= 255 for p in parts):
|
|
43
|
+
return True
|
|
44
|
+
except ValueError:
|
|
45
|
+
return False
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _loopback_bypass_on() -> bool:
|
|
50
|
+
"""【P3-loopback】本机免密开关。默认开(本机开发体验优先);
|
|
51
|
+
FOXCLAW_LOOPBACK_BYPASS=0 显式关(强迫症/特殊场景)。
|
|
52
|
+
与 _gateway_on 同模式:每请求读 env(运行时可配 + 便于测试)。
|
|
53
|
+
|
|
54
|
+
设计依据:RUN_CN.md:113「不设 FOXCLAW_API_KEY = 无鉴权,仅回环地址安全」。
|
|
55
|
+
→ 回环本身就安全,鉴权的目的是挡非回环访问,本机不该被一刀切。
|
|
56
|
+
→ 真正对外场景(0.0.0.0 监听)非 127.0.0.1 客户端仍要 key,锁门能力不变。
|
|
57
|
+
"""
|
|
58
|
+
v = os.environ.get("FOXCLAW_LOOPBACK_BYPASS", "").strip()
|
|
59
|
+
if v == "0":
|
|
60
|
+
return False
|
|
61
|
+
if v == "1":
|
|
62
|
+
return True
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _host_header_allowed(host_header: str) -> bool:
|
|
67
|
+
"""【P3-rebind】Host 头校验,防 DNS rebinding。
|
|
68
|
+
|
|
69
|
+
攻击模型(本机默认部署即中招):用户在本机跑 `foxclaw serve`(回环监听、无 key),
|
|
70
|
+
随后浏览器打开 evil.com;evil.com 的 DNS 解析到 127.0.0.1,于是页面内的
|
|
71
|
+
`fetch("http://evil.com:8000/api/...")` 实际打到本机 API,且与页面同源 →
|
|
72
|
+
恶意页可读写本地数据面(会话历史、记忆、触发执行链)。
|
|
73
|
+
|
|
74
|
+
本函数卡住的是「Host 头是公网域名」这一环:浏览器发请求时 Host 必须与被访问
|
|
75
|
+
域名一致,所以改解析也绕不过。规则:
|
|
76
|
+
- 无点主机名(localhost / testserver / 机器名)→ 放行;
|
|
77
|
+
- IP 字面量:回环与私网放行(家庭/公司局域网用法不受影响);
|
|
78
|
+
- 带点公网域名 → 默认拒绝,可用 FOXCLAW_ALLOWED_HOSTS 显式放行
|
|
79
|
+
(逗号分隔,支持 `*.example.com`)。
|
|
80
|
+
"""
|
|
81
|
+
raw = (host_header or "").strip().lower()
|
|
82
|
+
if not raw:
|
|
83
|
+
return True
|
|
84
|
+
if raw.startswith("["):
|
|
85
|
+
hostname = raw.split("]")[0].lstrip("[")
|
|
86
|
+
else:
|
|
87
|
+
hostname = raw.rsplit(":", 1)[0] if raw.count(":") == 1 else raw
|
|
88
|
+
hostname = hostname.rstrip(".")
|
|
89
|
+
if not hostname:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
import ipaddress
|
|
93
|
+
try:
|
|
94
|
+
ip = ipaddress.ip_address(hostname)
|
|
95
|
+
except ValueError:
|
|
96
|
+
ip = None
|
|
97
|
+
if ip is not None:
|
|
98
|
+
return ip.is_loopback or ip.is_private
|
|
99
|
+
|
|
100
|
+
allowed = [h.strip().lower() for h in
|
|
101
|
+
os.environ.get("FOXCLAW_ALLOWED_HOSTS", "").split(",") if h.strip()]
|
|
102
|
+
for pat in allowed:
|
|
103
|
+
if pat.startswith("*.") and hostname.endswith(pat[1:]):
|
|
104
|
+
return True
|
|
105
|
+
if hostname == pat:
|
|
106
|
+
return True
|
|
107
|
+
return "." not in hostname
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_guarded_path(path: str) -> bool:
|
|
111
|
+
"""【P3-8】需要鉴权/限流/追踪的数据面路径:/api/*、/knowledge/*、/config/*。
|
|
112
|
+
|
|
113
|
+
抽成辅助函数:此前 _api_key_guard / _gateway_mw / _trace_mw 各自重复
|
|
114
|
+
`startswith("/api") or startswith("/knowledge")`,未来加新前缀(如 /ws 之外的
|
|
115
|
+
新数据面)只改一处,也避免三处漂移出不一致的防护口径。
|
|
116
|
+
/knowledge/* 是 HKC 数据面(graph/ingest/reset),必须与 /api 同一防护口径。
|
|
117
|
+
/config/* 是 HKC 运行时 LLM 配置(连接设置),属可变状态,与数据面同口径保护。
|
|
118
|
+
/stats 是 HKC 统计(真实 data_dir + 全库 KU 总数),虽挂在应用根路径上,内容同属
|
|
119
|
+
数据面 —— 此前遗漏,设了 FOXCLAW_API_KEY 仍可匿名读取。
|
|
120
|
+
"""
|
|
121
|
+
return (path.startswith("/api") or path.startswith("/knowledge")
|
|
122
|
+
or path.startswith("/config")
|
|
123
|
+
or path == "/stats" or path.startswith("/stats/"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def _api_key_guard(request, call_next):
|
|
127
|
+
"""设了 FOXCLAW_API_KEY 时,保护 /api/* 与 /ws/*。未设则放行(本地默认)。
|
|
128
|
+
每请求读环境变量:可运行时配置,也便于测试。
|
|
129
|
+
|
|
130
|
+
【P3-loopback】本机客户端(127.0.0.1 / ::1)自动免密:仅对非 loopback 客户端
|
|
131
|
+
校验 X-API-Key。详见 _loopback_bypass_on。"""
|
|
132
|
+
api_key = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
133
|
+
path = request.url.path
|
|
134
|
+
if _is_guarded_path(path):
|
|
135
|
+
_client = request.client.host if request.client else ""
|
|
136
|
+
if _is_loopback_client(_client) and not _host_header_allowed(
|
|
137
|
+
request.headers.get("host", "")):
|
|
138
|
+
from fastapi.responses import JSONResponse
|
|
139
|
+
return JSONResponse(
|
|
140
|
+
{"error": "forbidden: Host 头不是本机地址(已启用 DNS rebinding 防护)。"
|
|
141
|
+
"自建域名请加入 FOXCLAW_ALLOWED_HOSTS。"}, status_code=403)
|
|
142
|
+
if api_key:
|
|
143
|
+
if _is_guarded_path(path):
|
|
144
|
+
if request.method == "OPTIONS" and request.headers.get("access-control-request-method"):
|
|
145
|
+
return await call_next(request)
|
|
146
|
+
if _loopback_bypass_on():
|
|
147
|
+
client_host = request.client.host if request.client else ""
|
|
148
|
+
if _is_loopback_client(client_host):
|
|
149
|
+
return await call_next(request)
|
|
150
|
+
if not _constant_time_eq(request.headers.get("X-API-Key", "") or "", api_key):
|
|
151
|
+
from fastapi.responses import JSONResponse
|
|
152
|
+
return JSONResponse(
|
|
153
|
+
{"error": "unauthorized: 缺少或无效的 X-API-Key"}, status_code=401)
|
|
154
|
+
return await call_next(request)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_GATEWAY = Gateway(
|
|
158
|
+
rate_per_min=settings.get_int("FOXCLAW_GATEWAY_RATE"),
|
|
159
|
+
quota_per_hour=settings.get_int("FOXCLAW_GATEWAY_QUOTA"),
|
|
160
|
+
)
|
|
161
|
+
_MAX_BODY = settings.get_int("FOXCLAW_MAX_BODY_MB") * 1024 * 1024
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _PayloadTooLarge(Exception):
|
|
165
|
+
"""请求体在**流上**超限:Content-Length 缺失或撒谎(如 chunked 传输)。"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, size: int):
|
|
168
|
+
super().__init__(size)
|
|
169
|
+
self.size = size
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _wrap_body_limit(request, max_bytes: int) -> None:
|
|
173
|
+
"""给 `request.receive` 套一层累计计数。
|
|
174
|
+
|
|
175
|
+
`Content-Length` 只是**声明值**:chunked 或不带该头时上游可以无限发,只查头等于没拦。
|
|
176
|
+
头检查保留(不读流就能拒掉绝大多数),这里对流兜底,两道都要。
|
|
177
|
+
"""
|
|
178
|
+
original = request.receive
|
|
179
|
+
received = 0
|
|
180
|
+
|
|
181
|
+
async def _receive():
|
|
182
|
+
nonlocal received
|
|
183
|
+
msg = await original()
|
|
184
|
+
if msg.get("type") == "http.request":
|
|
185
|
+
received += len(msg.get("body") or b"")
|
|
186
|
+
if received > max_bytes:
|
|
187
|
+
raise _PayloadTooLarge(received)
|
|
188
|
+
return msg
|
|
189
|
+
|
|
190
|
+
request._receive = _receive
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _gateway_on() -> bool:
|
|
194
|
+
"""网关是否启用。沿用鉴权哲学:配了 FOXCLAW_API_KEY(=对外暴露)才自动开;
|
|
195
|
+
FOXCLAW_GATEWAY=1 强制开、=0 强制关。每请求读环境变量(运行时可配 + 便于测试)。"""
|
|
196
|
+
v = os.environ.get("FOXCLAW_GATEWAY", "").strip()
|
|
197
|
+
if v == "1":
|
|
198
|
+
return True
|
|
199
|
+
if v == "0":
|
|
200
|
+
return False
|
|
201
|
+
return bool(os.environ.get("FOXCLAW_API_KEY", "").strip())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _gateway_mw(request, call_next):
|
|
205
|
+
"""统一网关:只管 /api 与 /knowledge 数据面(探针 /health、/metrics、/ 不在其下)。
|
|
206
|
+
client 身份:持有效 key → 按 key 计;否则按来源 IP 计(故轮换错误 key 无法绕过 IP 限流)。
|
|
207
|
+
超限 → 429 + Retry-After。默认关(本地);配鉴权或显式开启才生效。"""
|
|
208
|
+
path = request.url.path
|
|
209
|
+
guarded = _is_guarded_path(path)
|
|
210
|
+
if guarded:
|
|
211
|
+
cl = request.headers.get("content-length")
|
|
212
|
+
if cl and cl.isdigit() and int(cl) > _MAX_BODY:
|
|
213
|
+
from fastapi.responses import JSONResponse
|
|
214
|
+
return JSONResponse(
|
|
215
|
+
{"error": f"payload too large (>{_MAX_BODY // 1048576}MB)"}, status_code=413)
|
|
216
|
+
_wrap_body_limit(request, _MAX_BODY)
|
|
217
|
+
if guarded and _gateway_on():
|
|
218
|
+
valid = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
219
|
+
hdr = (request.headers.get("X-API-Key") or "").strip()
|
|
220
|
+
client = ("k:" + hdr) if (valid and _constant_time_eq(hdr, valid)) \
|
|
221
|
+
else ("ip:" + (request.client.host if request.client else "anon"))
|
|
222
|
+
allowed, retry, reason = _GATEWAY.check(client)
|
|
223
|
+
if not allowed:
|
|
224
|
+
from fastapi.responses import JSONResponse
|
|
225
|
+
return JSONResponse(
|
|
226
|
+
{"error": f"rate limited ({reason})", "retry_after": retry},
|
|
227
|
+
status_code=429, headers={"Retry-After": str(retry)})
|
|
228
|
+
try:
|
|
229
|
+
return await call_next(request)
|
|
230
|
+
except _PayloadTooLarge:
|
|
231
|
+
from fastapi.responses import JSONResponse
|
|
232
|
+
return JSONResponse(
|
|
233
|
+
{"error": f"payload too large (>{_MAX_BODY // 1048576}MB)"}, status_code=413)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def _trace_mw(request, call_next):
|
|
237
|
+
"""按请求开一条 trace(contextvars 会随 asyncio.to_thread 传到后台线程,故 Agent/LLM/
|
|
238
|
+
skill 里的 span 仍归属本次请求)。响应带 X-Trace-Id 便于定位。只追数据面,不污染静态/探针。"""
|
|
239
|
+
from ..core.tracing import start_trace, clear_trace, TRACES, parse_traceparent
|
|
240
|
+
path = request.url.path
|
|
241
|
+
if not _is_guarded_path(path):
|
|
242
|
+
return await call_next(request)
|
|
243
|
+
inbound = parse_traceparent(request.headers.get("traceparent"))
|
|
244
|
+
tr = start_trace(f"{request.method} {path}", trace_id=inbound)
|
|
245
|
+
try:
|
|
246
|
+
resp = await call_next(request)
|
|
247
|
+
try:
|
|
248
|
+
resp.headers["X-Trace-Id"] = tr.trace_id
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
return resp
|
|
252
|
+
finally:
|
|
253
|
+
tr.finish()
|
|
254
|
+
TRACES.put(tr)
|
|
255
|
+
clear_trace()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _metrics_mw(request, call_next):
|
|
259
|
+
"""埋点:记录每个请求的延迟 + 状态(按路由模板分组,ID 不炸表)。"""
|
|
260
|
+
import time as _t
|
|
261
|
+
t0 = _t.perf_counter()
|
|
262
|
+
status = 500
|
|
263
|
+
try:
|
|
264
|
+
resp = await call_next(request)
|
|
265
|
+
status = resp.status_code
|
|
266
|
+
return resp
|
|
267
|
+
finally:
|
|
268
|
+
ms = (_t.perf_counter() - t0) * 1000.0
|
|
269
|
+
route = request.scope.get("route")
|
|
270
|
+
ep = getattr(route, "path", None) or "unmatched"
|
|
271
|
+
try:
|
|
272
|
+
from ..core.metrics import METRICS
|
|
273
|
+
METRICS.record_request(f"{request.method} {ep}", status, ms)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def install_middleware(app):
|
|
279
|
+
"""按原注册顺序挂载 4 个 http 中间件(等价于原先的 @app.middleware 装饰)。
|
|
280
|
+
|
|
281
|
+
顺序即执行顺序(Starlette 中间件后加先执行),必须与搬移前完全一致:
|
|
282
|
+
_api_key_guard → _gateway_mw → _trace_mw → _metrics_mw。
|
|
283
|
+
"""
|
|
284
|
+
if os.environ.get("FOXCLAW_API_KEY", "").strip() and _loopback_bypass_on():
|
|
285
|
+
import logging
|
|
286
|
+
logging.getLogger("foxclaw").info(
|
|
287
|
+
"[LoopbackBypass] 已启用:本机(127.0.0.1 / ::1)请求自动免密,"
|
|
288
|
+
"鉴权仅作用于非 loopback 客户端。"
|
|
289
|
+
"设 FOXCLAW_LOOPBACK_BYPASS=0 可关闭。"
|
|
290
|
+
)
|
|
291
|
+
app.middleware("http")(_api_key_guard)
|
|
292
|
+
app.middleware("http")(_gateway_mw)
|
|
293
|
+
app.middleware("http")(_trace_mw)
|
|
294
|
+
app.middleware("http")(_metrics_mw)
|