foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -3,43 +3,31 @@ Foxclaw API — FastAPI 后端
|
|
|
3
3
|
前端控制台通过这里获取所有数据
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import threading
|
|
6
7
|
import os
|
|
7
8
|
import asyncio
|
|
8
9
|
import queue
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from contextlib import asynccontextmanager
|
|
11
12
|
|
|
12
|
-
# 加载项目根目录的 .env(若存在),让所有 Key 从配置文件读取。
|
|
13
|
-
#
|
|
14
|
-
# 【FOXCLAW_SKIP_DOTENV】跳过 .env,只用进程环境变量 —— 专供测试/CI 使用。
|
|
15
|
-
# 为什么必须有这个逃生口:load_dotenv(override=False) 只保护「进程里已存在的键」。
|
|
16
|
-
# 测试若先 pop 掉某键(如 test_gateway.py pop FOXCLAW_API_KEY 以验证「未设即开放」),
|
|
17
|
-
# 再 import 本模块,.env 就会把它灌回来 → 测试实际跑在开发者的真实配置上。
|
|
18
|
-
# 后果有三:(1) 同一份代码在不同机器上结果不同,回归不可复现;
|
|
19
|
-
# (2) .env 里的真实 Key 会让测试真的打到外部 API(产生费用);
|
|
20
|
-
# (3) FOXCLAW_DATA_DIR=./foxclaw_data 会让测试写进用户的真实数据目录。
|
|
21
|
-
# 设 FOXCLAW_SKIP_DOTENV=1 后,进程环境变量成为唯一来源,测试环境完全自洽。
|
|
22
13
|
try:
|
|
23
14
|
from ..core.envfile import load_env_file
|
|
24
15
|
_env_path = Path(__file__).parent.parent.parent / ".env"
|
|
25
16
|
if os.environ.get("FOXCLAW_SKIP_DOTENV", "").strip() in ("1", "true", "True", "yes"):
|
|
26
17
|
print("[Foxclaw] ○ FOXCLAW_SKIP_DOTENV=1 —— 跳过 .env,仅用进程环境变量")
|
|
27
18
|
elif _env_path.exists():
|
|
28
|
-
# 非空值优先:系统环境变量里的同名旧 key 不再压住 .env(见 core/envfile.py)
|
|
29
19
|
_shadows = load_env_file(_env_path)
|
|
30
20
|
print("[Foxclaw] ✓ 已加载配置文件 .env")
|
|
31
21
|
if _shadows:
|
|
32
22
|
print("[Foxclaw] ! .env 覆盖了系统环境变量: " + ", ".join(_shadows))
|
|
33
23
|
except ImportError:
|
|
34
|
-
pass
|
|
24
|
+
pass
|
|
35
25
|
|
|
36
26
|
from fastapi import FastAPI
|
|
37
27
|
from typing import Optional
|
|
38
28
|
|
|
39
29
|
from foxclaw import Foxclaw, __version__
|
|
40
30
|
|
|
41
|
-
# 中间件实现已移至 middleware.py;此处保留符号作 re-export(测试/外部经 foxclaw.api 访问),
|
|
42
|
-
# 冗余别名 `X as X` 让 ruff 识别为显式 re-export(F401 硬门)。install_middleware 在下方调用。
|
|
43
31
|
from .middleware import (
|
|
44
32
|
install_middleware,
|
|
45
33
|
_constant_time_eq as _constant_time_eq,
|
|
@@ -55,23 +43,15 @@ from .middleware import (
|
|
|
55
43
|
|
|
56
44
|
from .routes.ws import manager, harness_websocket as harness_websocket
|
|
57
45
|
|
|
58
|
-
# 事件脊柱辅助(WS 净化 / HKC 预热)已移至 spine.py;`_register_spine_projection` 因
|
|
59
|
-
# `_WS_MUTED` 源码级断言约束仍留本文件(见 test_governance_audit)。
|
|
60
46
|
from .spine import _ws_safe_payload, _prewarm_hkc
|
|
61
|
-
|
|
62
|
-
from .
|
|
47
|
+
from ._state import current_os, set_os_instance
|
|
48
|
+
from ._state import os_instance as os_instance
|
|
63
49
|
|
|
50
|
+
from .bootstrap import _configure_cors, _mount_hkc, _mount_frontend
|
|
64
51
|
|
|
65
|
-
# ── 全局 OS 实例 ───────────────────────────────────────────
|
|
66
|
-
_os_instance: Optional[Foxclaw] = None
|
|
67
|
-
|
|
68
|
-
# 线程安全事件队列:harness 在任意线程触发的事件先入队,
|
|
69
|
-
# 由后台 async 任务统一广播到 WebSocket(解决跨线程 create_task 失败问题)
|
|
70
52
|
_event_queue: "queue.Queue" = queue.Queue()
|
|
71
53
|
_broadcast_task: Optional[asyncio.Task] = None
|
|
72
|
-
|
|
73
|
-
# 预热线程仍在 build_index() → use-after-free → Windows access violation(偶发崩溃)。
|
|
74
|
-
_prewarm_thread = None
|
|
54
|
+
_prewarm_thread: "threading.Thread | None" = None
|
|
75
55
|
|
|
76
56
|
|
|
77
57
|
def _register_spine_projection(os_instance: Foxclaw):
|
|
@@ -80,10 +60,6 @@ def _register_spine_projection(os_instance: Foxclaw):
|
|
|
80
60
|
truth/process/memory 等系统事件被漏掉)。现在 harness 事件与系统事件同经脊柱,
|
|
81
61
|
一个投影全量观测。payload 经 _ws_safe_payload 净化(JSON 安全 + 有界),避免脏/大
|
|
82
62
|
对象丢连接或泛洪。event/data 形状与旧版一致(前端不受影响,未知事件被忽略)。"""
|
|
83
|
-
# 高频事件:给重放/审计用(仍在脊柱 history + robot sink),不广播到 WS 以免泛洪。
|
|
84
|
-
# · agent.cognition.*:逐 stage 认知事件
|
|
85
|
-
# · knowledge.created/updated:HKC 批量摄入一本书→数百条,量大;merged/conflict/
|
|
86
|
-
# ability/agent.used_ability 等"有意义"的知识事件仍广播(低频、可观测知识演化)。
|
|
87
63
|
_WS_MUTED = ("knowledge.created", "knowledge.updated")
|
|
88
64
|
|
|
89
65
|
def project(evt):
|
|
@@ -99,32 +75,25 @@ def _register_spine_projection(os_instance: Foxclaw):
|
|
|
99
75
|
|
|
100
76
|
@asynccontextmanager
|
|
101
77
|
async def lifespan(app: FastAPI):
|
|
102
|
-
global
|
|
103
|
-
# 启动自检:未设 FOXCLAW_API_KEY 即等于无鉴权。此处只做提醒(绑定地址由启动方式决定,
|
|
104
|
-
# 拿不到 host);__main__ 入口对「非回环 + 无鉴权」有硬拦截。
|
|
78
|
+
global _broadcast_task
|
|
105
79
|
if not os.environ.get("FOXCLAW_API_KEY", "").strip():
|
|
106
80
|
print("[Foxclaw API] ⚠ 未设置 FOXCLAW_API_KEY —— 当前无鉴权,"
|
|
107
81
|
"仅本地回环安全;对外暴露前务必设置。")
|
|
108
|
-
|
|
82
|
+
os_inst = Foxclaw(
|
|
109
83
|
data_dir=os.environ.get("FOXCLAW_DATA_DIR", "./foxclaw_data"),
|
|
110
84
|
shadow_rounds=settings.get_int("FOXCLAW_SHADOW_ROUNDS"),
|
|
111
85
|
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# 落库后才能"查昨天那次慢请求"。只留慢的/出错的 + 采样,见 tracing._should_persist。
|
|
86
|
+
set_os_instance(os_inst)
|
|
87
|
+
_register_spine_projection(os_inst)
|
|
115
88
|
try:
|
|
116
89
|
from ..core.tracing import TRACES
|
|
117
|
-
TRACES.attach_db(Path(
|
|
90
|
+
TRACES.attach_db(Path(os_inst.data_dir) / "traces.db")
|
|
118
91
|
except Exception as e:
|
|
119
92
|
print(f"[Foxclaw API] ⚠ trace 持久化未启用: {e}")
|
|
120
|
-
# 后台任务:把队列里的事件广播到所有 WebSocket
|
|
121
93
|
_broadcast_task = asyncio.create_task(_drain_event_queue())
|
|
122
94
|
print("[Foxclaw API] ✓ 事件广播任务已启动")
|
|
123
|
-
|
|
124
|
-
# 免得"重启后第一次知识查询"触发冷装配卡 ~1 分钟。后台守护线程跑、不阻塞启动;失败静默。
|
|
125
|
-
_prewarm_hkc(_os_instance)
|
|
95
|
+
_prewarm_hkc(os_inst)
|
|
126
96
|
if _foxclaw_mcp is not None:
|
|
127
|
-
# 内置 MCP 服务器的 session_manager 需在 app lifespan 内 run()
|
|
128
97
|
async with _foxclaw_mcp.session_manager.run():
|
|
129
98
|
print("[Foxclaw API] ✓ 内置 MCP 服务器已挂载 /api/mcp/")
|
|
130
99
|
yield
|
|
@@ -136,13 +105,11 @@ async def lifespan(app: FastAPI):
|
|
|
136
105
|
await _broadcast_task
|
|
137
106
|
except asyncio.CancelledError:
|
|
138
107
|
pass
|
|
139
|
-
# 释放持有原生资源的子系统(浏览器专用线程/会话),避免进程无法干净退出。
|
|
140
|
-
# 先 join HKC 预热线程:预热在后台线程对 SQLite 建索引,若不 join 就直接 aclose()
|
|
141
|
-
# 关闭 SQLite 连接,会与预热线程产生 use-after-free → Windows access violation。
|
|
142
108
|
if _prewarm_thread is not None and _prewarm_thread.is_alive():
|
|
143
109
|
_prewarm_thread.join(timeout=120.0)
|
|
144
|
-
|
|
145
|
-
|
|
110
|
+
live = current_os()
|
|
111
|
+
if live is not None:
|
|
112
|
+
await live.aclose()
|
|
146
113
|
print("[Foxclaw API] 关闭中...")
|
|
147
114
|
|
|
148
115
|
|
|
@@ -152,8 +119,6 @@ async def _drain_event_queue():
|
|
|
152
119
|
loop = asyncio.get_running_loop()
|
|
153
120
|
while True:
|
|
154
121
|
try:
|
|
155
|
-
# 带超时地阻塞取队列:超时让线程周期性醒来,
|
|
156
|
-
# 以便被 cancel 时 executor 线程能干净退出(否则进程无法结束)
|
|
157
122
|
try:
|
|
158
123
|
event = await loop.run_in_executor(
|
|
159
124
|
None, lambda: _event_queue.get(timeout=0.5))
|
|
@@ -166,14 +131,12 @@ async def _drain_event_queue():
|
|
|
166
131
|
await asyncio.sleep(0.1)
|
|
167
132
|
|
|
168
133
|
|
|
169
|
-
# ── 内置 MCP 服务器(默认关;FOXCLAW_MCP_SERVER=1 开)──────────
|
|
170
|
-
# 把只读知识+记忆暴露成 MCP 工具,挂 /api/mcp/(自动受 X-API-Key 中间件保护)。
|
|
171
134
|
_foxclaw_mcp = None
|
|
172
135
|
_foxclaw_mcp_app = None
|
|
173
136
|
if os.environ.get("FOXCLAW_MCP_SERVER", "0").strip() in ("1", "true", "True"):
|
|
174
137
|
from ..mcp_server import build_foxclaw_mcp
|
|
175
138
|
_mcp_agents = os.environ.get("FOXCLAW_MCP_AGENTS", "0").strip() in ("1", "true", "True")
|
|
176
|
-
_foxclaw_mcp = build_foxclaw_mcp(lambda:
|
|
139
|
+
_foxclaw_mcp = build_foxclaw_mcp(lambda: current_os(), expose_agents=_mcp_agents)
|
|
177
140
|
_foxclaw_mcp_app = _foxclaw_mcp.streamable_http_app()
|
|
178
141
|
if _mcp_agents:
|
|
179
142
|
print("[Foxclaw API] ⚠ MCP 已暴露 Agent 调度(ask_foxclaw)——外部客户端可驱动你的认知 Agent")
|
|
@@ -183,86 +146,64 @@ if os.environ.get("FOXCLAW_MCP_SERVER", "0").strip() in ("1", "true", "True"):
|
|
|
183
146
|
if os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip() == "*":
|
|
184
147
|
print("[Foxclaw API] ⚠ MCP 已开启但 CORS=* —— 生产建议用 FOXCLAW_CORS_ORIGINS 收紧允许来源")
|
|
185
148
|
|
|
186
|
-
app = FastAPI(
|
|
149
|
+
app: FastAPI = FastAPI(
|
|
187
150
|
title="Foxclaw API",
|
|
188
151
|
version=__version__,
|
|
189
152
|
description="Cognitive Agent Operating System",
|
|
190
153
|
lifespan=lifespan,
|
|
191
154
|
)
|
|
192
155
|
if _foxclaw_mcp_app is not None:
|
|
193
|
-
app.mount("/api/mcp", _foxclaw_mcp_app)
|
|
156
|
+
app.mount("/api/mcp", _foxclaw_mcp_app)
|
|
194
157
|
|
|
195
|
-
# ── CORS 收紧:实现已移至 app.py ───────────────────────────
|
|
196
158
|
_configure_cors(app)
|
|
197
159
|
|
|
198
160
|
|
|
199
|
-
# ── 中间件注册(鉴权/网关限流/追踪/指标):实现已移至 middleware.py,这里按原顺序集中挂载 ──
|
|
200
161
|
install_middleware(app)
|
|
201
162
|
|
|
202
|
-
# ── WebSocket 路由:实现已移至 routes/ws.py ────────────────
|
|
203
163
|
from .routes import ws as _ws
|
|
204
164
|
_ws.register(app)
|
|
205
165
|
|
|
206
|
-
# ── 系统/监控/追踪路由:实现已移至 routes/system.py(import 即注册) ────────────────
|
|
207
166
|
from .routes import system # noqa: F401
|
|
208
167
|
|
|
209
|
-
# ── AHS 执行链路由:实现已移至 routes/ahs.py(import 即注册) ────────────────
|
|
210
168
|
from .routes import ahs # noqa: F401
|
|
211
169
|
|
|
212
|
-
# ── 记忆/会话/偏好:routes/memory.py(import 即注册) ────────────────
|
|
213
170
|
from .routes import memory # noqa: F401
|
|
214
171
|
|
|
215
|
-
# ── 任务执行/流式直答/流式任务:routes/task.py(import 即注册) ────────────────
|
|
216
172
|
from .routes import task # noqa: F401
|
|
217
173
|
|
|
218
|
-
# ── 领域内核(财务/学习/行程):routes/domain.py(import 即注册) ────────────────
|
|
219
174
|
from .routes import domain # noqa: F401
|
|
220
175
|
|
|
221
|
-
# ── 断点续跑现场:routes/recovery.py(import 即注册) ────────────────
|
|
222
176
|
from .routes import recovery # noqa: F401
|
|
223
177
|
|
|
224
|
-
# ── 主管编排:routes/orchestration.py(import 即注册) ────────────────
|
|
225
178
|
from .routes import orchestration # noqa: F401
|
|
226
179
|
|
|
227
|
-
# ── 浏览器子系统:routes/browser.py(import 即注册) ────────────────
|
|
228
180
|
from .routes import browser # noqa: F401
|
|
229
181
|
|
|
230
|
-
# ── 知识核心(HKC):routes/knowledge.py(import 即注册) ────────────────
|
|
231
182
|
from .routes import knowledge # noqa: F401
|
|
232
183
|
|
|
233
|
-
# ── SM-2 复习面:routes/review.py(import 即注册) ────────────────
|
|
234
184
|
from .routes import review # noqa: F401
|
|
235
185
|
|
|
236
|
-
# ── 认知搜索(HSR):routes/search.py(import 即注册) ────────────────
|
|
237
186
|
from .routes import search # noqa: F401
|
|
238
187
|
|
|
239
|
-
# ── 聊天附件上传/下载(多模态渲染·步骤7):routes/attachments.py(import 即注册) ────────────────
|
|
240
188
|
from .routes import attachments # noqa: F401
|
|
241
189
|
|
|
242
|
-
# ── 前端 LLM 管理(下拉式选模型 + 自定义):routes/llm_admin.py(import 即注册) ──────
|
|
243
190
|
from .routes import llm_admin # noqa: F401
|
|
244
191
|
|
|
245
|
-
# 搬移前这些是 api 模块级符号,测试/外部仍经 foxclaw.api 访问 → re-export
|
|
246
192
|
from .routes.task import _sanitize_images, _sanitize_attachments, _MAX_IMAGES, _MAX_IMG_B64, _STREAM_LIMITER # noqa: F401
|
|
247
193
|
from .routes.domain import _parse_import_rows # noqa: F401
|
|
248
194
|
from .routes.knowledge import knowledge_ability as knowledge_ability, _HKCInsuffCov as _HKCInsuffCov # noqa: F401
|
|
249
195
|
from ..core import settings
|
|
250
196
|
|
|
251
197
|
|
|
252
|
-
# ── HKC 星球数据面:实现已移至 app.py ─────────────────────
|
|
253
198
|
_mount_hkc(app)
|
|
254
199
|
|
|
255
200
|
|
|
256
|
-
# ── 前端静态文件:实现已移至 app.py ─────────────────────────
|
|
257
201
|
_mount_frontend(app)
|
|
258
202
|
|
|
259
203
|
|
|
260
204
|
if __name__ == "__main__":
|
|
261
205
|
import uvicorn
|
|
262
206
|
|
|
263
|
-
# 默认只绑回环地址(本地开发)。对外暴露必须显式设 FOXCLAW_HOST,且应同时设置 FOXCLAW_API_KEY——
|
|
264
|
-
# 否则该端口上任何可达者都能读写记忆、执行技能、摄入文件(这些端点全部无鉴权)。
|
|
265
|
-
# 用 `python -m uvicorn foxclaw.api:app --host ...` 启动时会绕过本检查,故 lifespan 另有软提醒。
|
|
266
207
|
_host = os.environ.get("FOXCLAW_HOST", "127.0.0.1").strip() or "127.0.0.1"
|
|
267
208
|
_port = settings.get_int("FOXCLAW_PORT")
|
|
268
209
|
_LOOPBACK = {"127.0.0.1", "localhost", "::1", "[::1]"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""API 层共享 OS 实例的状态与访问器。
|
|
2
|
+
|
|
3
|
+
为什么独立成模块
|
|
4
|
+
----------------
|
|
5
|
+
`foxclaw/api/__init__.py` 受 15KB 单文件体积门(tools/size_check.py)约束,装配逻辑
|
|
6
|
+
已贴着上限。把「状态 + 访问器」放这里,既让 13 个路由文件能统一取实例,也不让主装配
|
|
7
|
+
文件继续膨胀。
|
|
8
|
+
|
|
9
|
+
读写契约
|
|
10
|
+
--------
|
|
11
|
+
写入只有 `set_os_instance()`(lifespan 启动时调用一次);读取两个入口:
|
|
12
|
+
|
|
13
|
+
- `os_instance()`:要求实例必须就绪,未就绪抛 503(可重试),用于正常请求路径
|
|
14
|
+
- `current_os()`:允许未就绪返回 None,用于「无 OS 也要降级返回」的只读路径
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, Optional
|
|
19
|
+
|
|
20
|
+
from fastapi import HTTPException
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from foxclaw import Foxclaw
|
|
24
|
+
|
|
25
|
+
_os_instance: Optional["Foxclaw"] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_os_instance(inst: "Foxclaw") -> None:
|
|
29
|
+
global _os_instance
|
|
30
|
+
_os_instance = inst
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def current_os() -> Optional["Foxclaw"]:
|
|
34
|
+
return _os_instance
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def os_instance() -> "Foxclaw":
|
|
38
|
+
"""取共享 OS 实例;未就绪时抛 503 而不是让调用方吃到 NoneType 属性错误。"""
|
|
39
|
+
inst = _os_instance
|
|
40
|
+
if inst is None:
|
|
41
|
+
raise HTTPException(
|
|
42
|
+
status_code=503, detail="service is starting up, retry shortly")
|
|
43
|
+
return inst
|
|
@@ -15,14 +15,6 @@ import foxclaw.api as _api
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _configure_cors(app):
|
|
18
|
-
# ── 可选鉴权 + CORS 收紧(部署安全)──────────────────────────
|
|
19
|
-
# FOXCLAW_API_KEY 未设 → 开放(本地默认,保持现状,既有测试/前端不受影响)。
|
|
20
|
-
# 设了 → 所有 /api/* 请求必须带正确的 X-API-Key,否则 401。
|
|
21
|
-
# 注意:http 中间件**看不到 websocket scope**,故 /ws 的鉴权在 WS 端点内单独做
|
|
22
|
-
# (见 harness_websocket,用 ?key= 查询参数)。前端(静态)与 /docs 始终开放。
|
|
23
|
-
# 默认只放行回环来源(本地开发足够:前端与 API 是同源挂载,同源请求本就不经 CORS)。
|
|
24
|
-
# 需要跨源时设 FOXCLAW_CORS_ORIGINS=来源1,来源2(逗号分隔);显式设 "*" 才会全开(不推荐,
|
|
25
|
-
# 与未鉴权叠加等于把 API 交给任意网页)。收紧默认值属于「默认安全」:不再默认 allow_origins=["*"]。
|
|
26
18
|
_cors_env = os.environ.get("FOXCLAW_CORS_ORIGINS", "").strip()
|
|
27
19
|
if _cors_env == "*":
|
|
28
20
|
_CORS_ORIGINS = ["*"]
|
|
@@ -44,12 +36,6 @@ def _configure_cors(app):
|
|
|
44
36
|
|
|
45
37
|
|
|
46
38
|
def _mount_hkc(app):
|
|
47
|
-
# ── HKC 星球数据面:挂载 HKC 自带的 /knowledge REST 路由 ──────────────────
|
|
48
|
-
# 星球 UI(frontend/planet)需要 /knowledge/graph、/ku/{id}、/ku/{id}/neighbors、
|
|
49
|
-
# /ingest/*、/reset 等。HKC 在本进程装配,这里把它自带的 knowledge 路由挂上,并用
|
|
50
|
-
# dependency_overrides 把 get_container 指向 **Foxclaw 已装配的同一个容器**(依赖倒置,
|
|
51
|
-
# 不另开一套;保持惰性:首个 /knowledge 请求才触发 HKC 装配)。鉴权由上面中间件覆盖 /knowledge。
|
|
52
|
-
# 缺 vendor/hkc 或导入失败 → 静默跳过(星球无数据但不影响主应用)。
|
|
53
39
|
try:
|
|
54
40
|
from foxclaw.knowledge import _ensure_hkc_importable as _ensure_hkc
|
|
55
41
|
_ensure_hkc()
|
|
@@ -57,33 +43,24 @@ def _mount_hkc(app):
|
|
|
57
43
|
from hkc_api.container import get_container as _hkc_get_container
|
|
58
44
|
|
|
59
45
|
def _resolve_hkc_container():
|
|
60
|
-
|
|
46
|
+
os_inst = _api.current_os()
|
|
47
|
+
svc = os_inst.hkc if os_inst is not None else None
|
|
61
48
|
if svc is None or not getattr(svc, "available", False):
|
|
62
49
|
raise HTTPException(503, "HKC 未启用(未 vendor 或缺依赖)")
|
|
63
50
|
return svc.container
|
|
64
51
|
|
|
65
52
|
app.dependency_overrides[_hkc_get_container] = _resolve_hkc_container
|
|
66
53
|
app.include_router(_hkc_knowledge_router)
|
|
67
|
-
# /stats 系统统计:独立 hkc-api 在 main.py 顶层 @app.get("/stats") 定义,不在任何
|
|
68
|
-
# router 里,嵌入式挂载时会漏掉 → 星球 UI 的 updateDataInfo 调 /stats 404,
|
|
69
|
-
# 「数据目录 / 全库知识总数」提示失效。这里补挂,复用同一容器 override(惰性装配)。
|
|
70
54
|
@app.get("/stats", tags=["system"])
|
|
71
55
|
def _hkc_stats():
|
|
72
56
|
return _resolve_hkc_container().stats()
|
|
73
|
-
# 连接设置(星球 UI 左下的「连接设置」面板)POST /config/llm 做运行时 LLM 配置,
|
|
74
|
-
# 覆盖 .env 的 HKC_LLM_*。独立 hkc-api 有这个路由,嵌入式 foxclaw 之前没挂 → 前端
|
|
75
|
-
# 报「模型配置失败:404」且改配置无效。这里补挂 config 路由(同一容器 override 已覆盖),
|
|
76
|
-
# 让「连接设置」的「知识抽取模型」段在 foxclaw 下真正生效。
|
|
77
57
|
from hkc_api.routes.config import router as _hkc_config_router
|
|
78
|
-
app.include_router(_hkc_config_router)
|
|
79
|
-
# 星球副面板(冲突裁决/能力编译/最短路径)用的 HKC 路由。不挂的话 /conflicts、
|
|
80
|
-
# /abilities、/search/path 在 Foxclaw 下 404 → 星球「冲突裁决」面板拉不到数据、
|
|
81
|
-
# 永远显示 0(误导),能力/路径面板同样空。这里补挂(同一容器 override 已覆盖)。
|
|
58
|
+
app.include_router(_hkc_config_router)
|
|
82
59
|
try:
|
|
83
60
|
from hkc_api.routes.ability import router as _hkc_ability_router
|
|
84
61
|
from hkc_api.routes.search import router as _hkc_search_router
|
|
85
|
-
app.include_router(_hkc_ability_router)
|
|
86
|
-
app.include_router(_hkc_search_router)
|
|
62
|
+
app.include_router(_hkc_ability_router)
|
|
63
|
+
app.include_router(_hkc_search_router)
|
|
87
64
|
_sub = "/abilities, /conflicts, /search/*"
|
|
88
65
|
except Exception as _e2:
|
|
89
66
|
_sub = f"(能力/搜索路由未挂: {_e2})"
|
|
@@ -93,10 +70,6 @@ def _mount_hkc(app):
|
|
|
93
70
|
|
|
94
71
|
|
|
95
72
|
def _mount_frontend(app):
|
|
96
|
-
# ── 前端静态文件 ───────────────────────────────────────────
|
|
97
|
-
# frontend/ 下现在并存三套:frontend/v3(Vue,当前主界面)、deck_v2.html(旧主界面)、
|
|
98
|
-
# deck.html(最早的一版)。根路径必须给 v3 —— 之前一直指向 deck_v2,新版只能手输 /v3/,
|
|
99
|
-
# 等于新界面根本没上线。
|
|
100
73
|
frontend_dir = Path(__file__).parent.parent.parent / "frontend"
|
|
101
74
|
if not frontend_dir.exists():
|
|
102
75
|
return
|
|
@@ -60,7 +60,7 @@ def _loopback_bypass_on() -> bool:
|
|
|
60
60
|
return False
|
|
61
61
|
if v == "1":
|
|
62
62
|
return True
|
|
63
|
-
return True
|
|
63
|
+
return True
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def _host_header_allowed(host_header: str) -> bool:
|
|
@@ -80,8 +80,8 @@ def _host_header_allowed(host_header: str) -> bool:
|
|
|
80
80
|
"""
|
|
81
81
|
raw = (host_header or "").strip().lower()
|
|
82
82
|
if not raw:
|
|
83
|
-
return True
|
|
84
|
-
if raw.startswith("["):
|
|
83
|
+
return True
|
|
84
|
+
if raw.startswith("["):
|
|
85
85
|
hostname = raw.split("]")[0].lstrip("[")
|
|
86
86
|
else:
|
|
87
87
|
hostname = raw.rsplit(":", 1)[0] if raw.count(":") == 1 else raw
|
|
@@ -90,7 +90,7 @@ def _host_header_allowed(host_header: str) -> bool:
|
|
|
90
90
|
return True
|
|
91
91
|
|
|
92
92
|
import ipaddress
|
|
93
|
-
try:
|
|
93
|
+
try:
|
|
94
94
|
ip = ipaddress.ip_address(hostname)
|
|
95
95
|
except ValueError:
|
|
96
96
|
ip = None
|
|
@@ -104,7 +104,7 @@ def _host_header_allowed(host_header: str) -> bool:
|
|
|
104
104
|
return True
|
|
105
105
|
if hostname == pat:
|
|
106
106
|
return True
|
|
107
|
-
return "." not in hostname
|
|
107
|
+
return "." not in hostname
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def _is_guarded_path(path: str) -> bool:
|
|
@@ -131,8 +131,6 @@ async def _api_key_guard(request, call_next):
|
|
|
131
131
|
校验 X-API-Key。详见 _loopback_bypass_on。"""
|
|
132
132
|
api_key = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
133
133
|
path = request.url.path
|
|
134
|
-
# 【P3-rebind】与是否配 key 无关:本机开发(无 key、回环监听)才是 DNS rebinding
|
|
135
|
-
# 的典型受害场景,所以这层放在 api_key 分支之外,默认部署也生效。
|
|
136
134
|
if _is_guarded_path(path):
|
|
137
135
|
_client = request.client.host if request.client else ""
|
|
138
136
|
if _is_loopback_client(_client) and not _host_header_allowed(
|
|
@@ -142,21 +140,13 @@ async def _api_key_guard(request, call_next):
|
|
|
142
140
|
{"error": "forbidden: Host 头不是本机地址(已启用 DNS rebinding 防护)。"
|
|
143
141
|
"自建域名请加入 FOXCLAW_ALLOWED_HOSTS。"}, status_code=403)
|
|
144
142
|
if api_key:
|
|
145
|
-
# /knowledge/* 是 HKC 星球的数据面(graph/ingest/reset),必须同 /api 一起鉴权,
|
|
146
|
-
# 否则 ingest/reset/graph 对公众开放(泄密+可乱写)。/ws 由端点内部鉴权。
|
|
147
143
|
if _is_guarded_path(path):
|
|
148
|
-
# CORS 预检(OPTIONS + Access-Control-Request-Method)按规范**不携带**自定义请求头,
|
|
149
|
-
# 浏览器也不会给它带 X-API-Key。必须放行交给 CORSMiddleware 处理;否则一旦启用鉴权,
|
|
150
|
-
# 跨源请求会在预检阶段被 401 拦死(且响应无 CORS 头,浏览器只报 CORS 错误,极难定位)。
|
|
151
144
|
if request.method == "OPTIONS" and request.headers.get("access-control-request-method"):
|
|
152
145
|
return await call_next(request)
|
|
153
|
-
# 【P3-loopback】本机回环自动放行:与 docs 哲学「回环地址安全」对齐,
|
|
154
|
-
# 避免本地开发时浏览器填 key 的体验摩擦。设 FOXCLAW_LOOPBACK_BYPASS=0 可关。
|
|
155
146
|
if _loopback_bypass_on():
|
|
156
147
|
client_host = request.client.host if request.client else ""
|
|
157
148
|
if _is_loopback_client(client_host):
|
|
158
149
|
return await call_next(request)
|
|
159
|
-
# 【P3-1】常量时间比对,防对 API Key 的计时攻击(MCP 暴露后更重要)
|
|
160
150
|
if not _constant_time_eq(request.headers.get("X-API-Key", "") or "", api_key):
|
|
161
151
|
from fastapi.responses import JSONResponse
|
|
162
152
|
return JSONResponse(
|
|
@@ -164,13 +154,10 @@ async def _api_key_guard(request, call_next):
|
|
|
164
154
|
return await call_next(request)
|
|
165
155
|
|
|
166
156
|
|
|
167
|
-
# ── 统一 API 网关:per-client 限流 + 配额(把散落的单点限流收敛成一层)──────────
|
|
168
157
|
_GATEWAY = Gateway(
|
|
169
158
|
rate_per_min=settings.get_int("FOXCLAW_GATEWAY_RATE"),
|
|
170
159
|
quota_per_hour=settings.get_int("FOXCLAW_GATEWAY_QUOTA"),
|
|
171
160
|
)
|
|
172
|
-
# 请求体硬顶(始终生效,不依赖限流开关):防超大 body 在解析/sanitize 前就吃满内存。
|
|
173
|
-
# 最大合法体是文件摄入(路由内 20MB 上限),故 32MB 顶对正常流量无感。
|
|
174
161
|
_MAX_BODY = settings.get_int("FOXCLAW_MAX_BODY_MB") * 1024 * 1024
|
|
175
162
|
|
|
176
163
|
|
|
@@ -220,21 +207,16 @@ async def _gateway_mw(request, call_next):
|
|
|
220
207
|
超限 → 429 + Retry-After。默认关(本地);配鉴权或显式开启才生效。"""
|
|
221
208
|
path = request.url.path
|
|
222
209
|
guarded = _is_guarded_path(path)
|
|
223
|
-
# ① 请求体大小硬顶(始终生效):Content-Length 超限 → 413,在读 body 前拦下
|
|
224
210
|
if guarded:
|
|
225
211
|
cl = request.headers.get("content-length")
|
|
226
212
|
if cl and cl.isdigit() and int(cl) > _MAX_BODY:
|
|
227
213
|
from fastapi.responses import JSONResponse
|
|
228
214
|
return JSONResponse(
|
|
229
215
|
{"error": f"payload too large (>{_MAX_BODY // 1048576}MB)"}, status_code=413)
|
|
230
|
-
# chunked / 无 Content-Length:靠流上累计兜底(头检查只是快路径)
|
|
231
216
|
_wrap_body_limit(request, _MAX_BODY)
|
|
232
|
-
# ② per-client 限流 + 配额(网关启用时)
|
|
233
217
|
if guarded and _gateway_on():
|
|
234
218
|
valid = os.environ.get("FOXCLAW_API_KEY", "").strip()
|
|
235
219
|
hdr = (request.headers.get("X-API-Key") or "").strip()
|
|
236
|
-
# 【P3-1】常量时间比较:普通 == 的短路耗时可被用来逐位猜 key,
|
|
237
|
-
# 把请求从 ip:* 伪造成 k:* 身份绕过 IP 限流
|
|
238
220
|
client = ("k:" + hdr) if (valid and _constant_time_eq(hdr, valid)) \
|
|
239
221
|
else ("ip:" + (request.client.host if request.client else "anon"))
|
|
240
222
|
allowed, retry, reason = _GATEWAY.check(client)
|
|
@@ -258,8 +240,6 @@ async def _trace_mw(request, call_next):
|
|
|
258
240
|
path = request.url.path
|
|
259
241
|
if not _is_guarded_path(path):
|
|
260
242
|
return await call_next(request)
|
|
261
|
-
# 跨进程接链:外部(如经 /api/mcp 调进来的 Agent)带了 W3C traceparent 就复用其 trace_id,
|
|
262
|
-
# 这样两边的链路能对齐;非法/缺失则自己开一条(不信外部输入)。
|
|
263
243
|
inbound = parse_traceparent(request.headers.get("traceparent"))
|
|
264
244
|
tr = start_trace(f"{request.method} {path}", trace_id=inbound)
|
|
265
245
|
try:
|
|
@@ -286,7 +266,6 @@ async def _metrics_mw(request, call_next):
|
|
|
286
266
|
return resp
|
|
287
267
|
finally:
|
|
288
268
|
ms = (_t.perf_counter() - t0) * 1000.0
|
|
289
|
-
# 只按已匹配的路由模板分组;未匹配(404)统一记 "unmatched",防攻击者刷不同路径撑爆 key
|
|
290
269
|
route = request.scope.get("route")
|
|
291
270
|
ep = getattr(route, "path", None) or "unmatched"
|
|
292
271
|
try:
|
|
@@ -302,8 +281,6 @@ def install_middleware(app):
|
|
|
302
281
|
顺序即执行顺序(Starlette 中间件后加先执行),必须与搬移前完全一致:
|
|
303
282
|
_api_key_guard → _gateway_mw → _trace_mw → _metrics_mw。
|
|
304
283
|
"""
|
|
305
|
-
# 【P3-loopback】启动审计日志:鉴权开启 + loopback bypass 默认开 → 打一行告知
|
|
306
|
-
# 运维者。设了 FOXCLAW_API_KEY 但又不想本机免密,显式 FOXCLAW_LOOPBACK_BYPASS=0。
|
|
307
284
|
if os.environ.get("FOXCLAW_API_KEY", "").strip() and _loopback_bypass_on():
|
|
308
285
|
import logging
|
|
309
286
|
logging.getLogger("foxclaw").info(
|