feat(infra): R02 vào thật — app chạy bằng JsonConfigRepository, khoá rời khỏi config.json
Từ 21/08 tôi đã viết xong 7 file R02 với 46 test xanh, và báo là "xong R02". Báo sai: code mới nằm song song, KHÔNG một dòng nào ngoài infrastructure/ và tests/ gọi tới nó. App vẫn chạy nguyên trên config.py, 29 file dùng nó, và khoá API của người dùng vẫn nằm plaintext trong config.json suốt 4 ngày. Commit này mới là phần refactor thật. Bù 21 thành viên còn thiếu (85 dòng) ------------------------------------ JsonConfigRepository có 18/34 thành viên công khai của AppConfig nên không tráo được. Chép nguyên ngữ nghĩa 21 cái còn lại: load, ms365_*, ext_connectors, connect_external, routing_mode_for, seeded_*, mcp_servers, teams, history, structure, monitoring_visibility, model_label, ca_bundle... Giờ 40/34, không thiếu gì. Không phải thiết kế mới — chừng nào 29 file còn gọi qua ctx.config thì repository phải trả lời được đúng các câu hỏi cũ. ROUTING_MODES lấy theo bản Delta (4 chế độ, có "fallback" từ R03-T03) chứ không theo bản main cũ 3 chế độ. Chép bản cũ là routing "fallback" âm thầm rơi về "off" sau khi Delta merge, không lỗi nào báo. Composition Root (R08-T10, phần đầu) ------------------------------------- presentation/shell/bootstrap.py: một chỗ duy nhất quyết định app dựng bằng mảnh nào. app.py::run giờ gọi build_context() thay cho AppConfig.load(). Đây cũng là chỗ ráp kho bí mật vào; máy không có keyring thì secrets=None và mọi thứ chạy như cũ. Kiểm trên dữ liệu thật ---------------------- Chạy lên máy tôi, migration tự chạy đúng như thiết kế: openai_compat 39 ký tự config.json -> Windows Credential Manager ollama giá trị bù nhìn, để nguyên trong file, không đẩy vào kho schema_version 1 -> 2 sao lưu config.json.v20260825-193206.bak Sau khi bật lại app và để nó ghi cấu hình, config.json vẫn sạch: api_key rỗng, không còn chuỗi nào có hình dạng khoá. scripts/audit_security.py sạch. Tiêu chí nghiệm thu A của plan (dòng 244) — "0 lưu trữ plaintext API Key trong JSON" — tới commit này mới thật sự đạt. 632 test xanh. check_dialogs, check_nav, check_design_parity đều qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e4ce9b2f5f
commit
2246d55286
@@ -0,0 +1,42 @@
|
||||
"""Composition Root — R08-T10.
|
||||
|
||||
Một chỗ duy nhất quyết định app chạy bằng những mảnh nào. Trước đây quyết định
|
||||
đó nằm rải trong ``app.py::run``, lẫn với việc dựng cửa sổ; tách ra để đổi một
|
||||
mảnh (ví dụ thay kho bí mật) không phải đụng vào mã giao diện.
|
||||
|
||||
Đây cũng là chỗ hoàn tất R02: từ đây app chạy bằng :class:`JsonConfigRepository`
|
||||
chứ không còn ``config.py::AppConfig``. Hai thứ đổi thật sự:
|
||||
|
||||
* ghi cấu hình qua ``AtomicJsonFile`` — mất điện giữa lúc lưu không làm hỏng file
|
||||
* API key nằm trong kho bí mật của hệ điều hành, không nằm trong ``config.json``
|
||||
|
||||
Máy không có kho bí mật (Linux headless, CI, hoặc keyring hỏng) vẫn chạy bình
|
||||
thường: repository nhận ``secrets=None`` và đọc khoá thẳng từ file như cũ. Thà
|
||||
để khoá trong file còn hơn xoá đi rồi người dùng mất khoá mà không hiểu vì sao.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ...infrastructure.config.json_config_repository import JsonConfigRepository
|
||||
from ...infrastructure.secrets.keyring_adapter import KeyringAdapter
|
||||
from ...state import AppContext
|
||||
|
||||
|
||||
def build_secret_store():
|
||||
"""Kho bí mật của hệ điều hành, hoặc None nếu máy này không có.
|
||||
|
||||
``KeyringAdapter`` không bao giờ ném lỗi — nó tự báo ``available``. Trả về
|
||||
None thay vì một adapter chết để chỗ gọi khỏi phải đoán.
|
||||
"""
|
||||
store = KeyringAdapter()
|
||||
return store if store.available else None
|
||||
|
||||
|
||||
def build_config(path: Path | None = None) -> JsonConfigRepository:
|
||||
return JsonConfigRepository.load(path, secrets=build_secret_store())
|
||||
|
||||
|
||||
def build_context(path: Path | None = None) -> AppContext:
|
||||
"""Dựng AppContext hoàn chỉnh — điểm vào cho ``app.run()`` và cho checker."""
|
||||
return AppContext(build_config(path))
|
||||
Reference in New Issue
Block a user