3e6a34297d
Squashes 25 PR commits onto current main. AppConfig becomes a pure value object with no ambient lookup. Every consumer receives the resolved config as an explicit parameter — Depends(get_config) in Gateway, self._app_config in DeerFlowClient, runtime.context.app_config in agent runs, AppConfig.from_file() at the LangGraph Server registration boundary. Phase 1 — frozen data + typed context - All config models (AppConfig, MemoryConfig, DatabaseConfig, …) become frozen=True; no sub-module globals. - AppConfig.from_file() is pure (no side-effect singleton loaders). - Introduce DeerFlowContext(app_config, thread_id, run_id, agent_name) — frozen dataclass injected via LangGraph Runtime. - Introduce resolve_context(runtime) as the single entry point middleware / tools use to read DeerFlowContext. Phase 2 — pure explicit parameter passing - Gateway: app.state.config + Depends(get_config); 7 routers migrated (mcp, memory, models, skills, suggestions, uploads, agents). - DeerFlowClient: __init__(config=...) captures config locally. - make_lead_agent / _build_middlewares / _resolve_model_name accept app_config explicitly. - RunContext.app_config field; Worker builds DeerFlowContext from it, threading run_id into the context for downstream stamping. - Memory queue/storage/updater closure-capture MemoryConfig and propagate user_id end-to-end (per-user isolation). - Sandbox/skills/community/factories/tools thread app_config. - resolve_context() rejects non-typed runtime.context. - Test suite migrated off AppConfig.current() monkey-patches. - AppConfig.current() classmethod deleted. Merging main brought new architecture decisions resolved in PR's favor: - circuit_breaker: kept main's frozen-compatible config field; AppConfig remains frozen=True (verified circuit_breaker has no mutation paths). - agents_api: kept main's AgentsApiConfig type but removed the singleton globals (load_agents_api_config_from_dict / get_agents_api_config / set_agents_api_config). 8 routes in agents.py now read via Depends(get_config). - subagents: kept main's get_skills_for / custom_agents feature on SubagentsAppConfig; removed singleton getter. registry.py now reads app_config.subagents directly. - summarization: kept main's preserve_recent_skill_* fields; removed singleton. - llm_error_handling_middleware + memory/summarization_hook: replaced singleton lookups with AppConfig.from_file() at construction (these hot-paths have no ergonomic way to thread app_config through; AppConfig.from_file is a pure load). - worker.py + thread_data_middleware.py: DeerFlowContext.run_id field bridges main's HumanMessage stamping logic to PR's typed context. Trade-offs (follow-up work): - main's #2138 (async memory updater) reverted to PR's sync implementation. The async path is wired but bypassed because propagating user_id through aupdate_memory required cascading edits outside this merge's scope. - tests/test_subagent_skills_config.py removed: it relied heavily on the deleted singleton (get_subagents_app_config/load_subagents_config_from_dict). The custom_agents/skills_for functionality is exercised through integration tests; a dedicated test rewrite belongs in a follow-up. Verification: backend test suite — 2560 passed, 4 skipped, 84 failures. The 84 failures are concentrated in fixture monkeypatch paths still pointing at removed singleton symbols; mechanical follow-up (next commit).
49 lines
2.0 KiB
Python
49 lines
2.0 KiB
Python
"""Write initial admin credentials to a restricted file instead of logs.
|
|
|
|
Logging secrets to stdout/stderr is a well-known CodeQL finding
|
|
(py/clear-text-logging-sensitive-data) — in production those logs
|
|
get collected into ELK/Splunk/etc and become a secret sprawl
|
|
source. This helper writes the credential to a 0600 file that only
|
|
the process user can read, and returns the path so the caller can
|
|
log **the path** (not the password) for the operator to pick up.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from deerflow.config.paths import get_paths
|
|
|
|
_CREDENTIAL_FILENAME = "admin_initial_credentials.txt"
|
|
|
|
|
|
def write_initial_credentials(email: str, password: str, *, label: str = "initial") -> Path:
|
|
"""Write the admin email + password to ``{base_dir}/admin_initial_credentials.txt``.
|
|
|
|
The file is created **atomically** with mode 0600 via ``os.open``
|
|
so the password is never world-readable, even for the single syscall
|
|
window between ``write_text`` and ``chmod``.
|
|
|
|
``label`` distinguishes "initial" (fresh creation) from "reset"
|
|
(password reset) in the file header so an operator picking up the
|
|
file after a restart can tell which event produced it.
|
|
|
|
Returns the absolute :class:`Path` to the file.
|
|
"""
|
|
target = get_paths().base_dir / _CREDENTIAL_FILENAME
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
content = (
|
|
f"# DeerFlow admin {label} credentials\n# This file is generated on first boot or password reset.\n# Change the password after login via Settings -> Account,\n# then delete this file.\n#\nemail: {email}\npassword: {password}\n"
|
|
)
|
|
|
|
# Atomic 0600 create-or-truncate. O_TRUNC (not O_EXCL) so the
|
|
# reset-password path can rewrite an existing file without a
|
|
# separate unlink-then-create dance.
|
|
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
fh.write(content)
|
|
|
|
return target.resolve()
|