From 8b2c26c04cb5ecdcfbfd9b5ae848d297fc1d86a2 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Tue, 9 Jun 2026 23:45:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(config):=20config=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=84=B0=EB=A6=AC=20=EA=B8=B0=EB=B0=98=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20(--config-path/--env-file,=20trex-orb-v1?= =?UTF-8?q?=20=EC=A0=95=ED=95=A9,=20BAT-026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REQ-048/010 change-req. `--config` 를 이름으로 해석 + `--config-path ` (기본 ./config) + resolve_config_toml(슬래시 포함 값은 하위호환 명시 경로). `.env` 를 config-dir 결정적 해석(--env-file 우선, 없으면 /.env, CWD 무관) + config/.env.example + .gitignore config/* 허용목록. 운영자 CLI 도 --config-path/--env-file 동일 적용. from_env() legacy 루트 .env 폴백 유지로 완전 하위호환. 단위 23 + 전체 375 passed, ruff clean, 라이브 스모크(name·path 동일). REQ-048/010 → Implemented. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 11 ++- config/.env.example | 57 +++++++++++ .../index.md | 34 +++++++ .../planning.md | 32 ++++++ .../verification.md | 45 +++++++++ docs/batches/index.md | 1 + .../req-010_daemon-dotenv-loading.md | 97 +++++++++++++++++-- .../req-048_per-instance-toml-config.md | 87 ++++++++++++++++- docs/srs/index.md | 2 +- tests/runtime/test_config_resolution.py | 87 +++++++++++++++++ tests/runtime/test_instance_config.py | 8 +- trex_core/runtime/cli.py | 25 ++++- trex_core/runtime/config.py | 59 +++++++++-- trex_core/runtime/daemon.py | 18 +++- 14 files changed, 531 insertions(+), 32 deletions(-) create mode 100644 config/.env.example create mode 100644 docs/batches/bat-026_20260609_config-dir-env-and-config-path/index.md create mode 100644 docs/batches/bat-026_20260609_config-dir-env-and-config-path/planning.md create mode 100644 docs/batches/bat-026_20260609_config-dir-env-and-config-path/verification.md create mode 100644 tests/runtime/test_config_resolution.py diff --git a/.gitignore b/.gitignore index 60394ac..bd2edaa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ __pycache__/ *.egg-info/ .pytest_cache/ /.env +**/.env /.vscode/ -# BAT-013 / REQ-048: per-instance TOML configs hold instance_key + portfolio_hub_token -# (secrets) — never commit. The tracked example below is exempt. -/config/*.toml -!/config/instance.example.toml +# REQ-048/010 CHG-20260609-01 (trex-orb-v1 aligned): config/ holds real per-instance +# TOML configs + the shared .env (secrets) — never commit. Only example templates +# (*.example.toml, .env.example) are tracked. +/config/* +!/config/*.example.toml +!/config/.env.example diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..93e03ad --- /dev/null +++ b/config/.env.example @@ -0,0 +1,57 @@ +# trex-core shared infrastructure + secrets (12-factor, REQ-010 CHG-20260609-01). +# +# These are SHARED across all per-instance TOML configs in this directory (same +# DB / broker / portfolio-hub / RabbitMQ), so they live here instead of the +# per-instance TOML. +# +# Resolution (trex_core.runtime.config): +# config_dir = --config-path (default ./config) +# --env-file wins, else /.env +# (standalone reads with no config fall back to legacy CWD-relative .env) +# Real exported environment variables ALWAYS take precedence over this file. +# Use --config-path to stay CWD-independent when launching elsewhere. +# +# Copy this template to `.env` in the same config dir and fill in: +# cp config/.env.example config/.env + +# --- Core (required) --- +# psycopg DSN for the PostgreSQL single-writer store. +TREX_POSTGRESQL_DSN=dbname=trex_core host=/var/run/postgresql + +# --- Daemon (optional, defaults shown) --- +TREX_TOTAL_SLOTS=10 +TREX_EXECUTION_INTERVAL_SEC=1.0 + +# --- Broker selection (REQ-011/012/013) --- +# 'paper' (deterministic test double, default) | 'kis' (real KIS Open API) +TREX_BROKER=paper + +# --- Paper broker tuning (only when TREX_BROKER=paper; for e2e scenarios) --- +# TREX_PAPER_FILL_AFTER_POLLS=0 +# TREX_PAPER_QTY_PER_POLL= +# TREX_PAPER_REJECT_SYMBOLS= + +# --- KIS broker (only when TREX_BROKER=kis) --- +# 모의투자(sandbox): https://openapivts.koreainvestment.com:29443 +# 실계좌(live): https://openapi.koreainvestment.com:9443 (requires TREX_KIS_ALLOW_LIVE=true) +TREX_KIS_BROKER_BASE_URL=https://openapivts.koreainvestment.com:29443 +TREX_KIS_BROKER_TIMEOUT=5 +TREX_KIS_BROKER_APP_KEY= +TREX_KIS_BROKER_APP_SECRET= +TREX_KIS_BROKER_ACCOUNT_REF=12345678-01 +TREX_KIS_ALLOW_LIVE=false +TREX_KIS_REDIS_URL= + +# --- portfolio-hub ledger (REQ-017/018/019, BAT-006) --- +# portfolio_hub_token is per-instance and lives in the instance TOML, not here. +TREX_PORTFOLIO_HUB_BASE_URL= +TREX_PORTFOLIO_HUB_TIMEOUT=5 +TREX_LEDGER_INTERVAL_SEC=1.0 + +# --- Snapshot scheduler (REQ-030, BAT-008) --- +TREX_SNAPSHOT_SCHEDULE_KST=07:30 +TREX_SNAPSHOT_TICK_SEC=60 + +# --- RabbitMQ transport (REQ-022, BAT-007) --- +TREX_RABBITMQ_URL= +TREX_RABBITMQ_TIMEOUT=5 diff --git a/docs/batches/bat-026_20260609_config-dir-env-and-config-path/index.md b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/index.md new file mode 100644 index 0000000..34043b1 --- /dev/null +++ b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/index.md @@ -0,0 +1,34 @@ +# BAT-026: config 디렉터리 기반 설정 관리 — `--config-path`/`--env-file` + config-dir `.env` 공용화 (trex-orb-v1 정합) + +- Status: release-candidate +- Profile: batch-lite +- Source Discovery: 없음 (trex-orb-v1 설정 관리 정합 — change-req 주도) +- Included REQ: REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01) +- Excluded REQ: 없음 + +## Summary +- trex-orb-v1의 config 디렉터리 기반 설정 관리(`--config ` + `--config-path ` + config-dir `.env`)를 trex-core 에 이식한다. +- 핵심: `resolve_config_toml(name, config_path)`(이름→`/.toml`, `.toml` 자동 부착, **슬래시 포함 값은 하위호환 명시 경로**)(REQ-048), `.env` 를 config-dir 기준 결정적 해석(`--env-file` 우선 → `/.env`)으로 변경 + `config/.env.example` + `.gitignore` `config/*` 허용목록(REQ-010). 데몬·운영자 CLI 양쪽에 `--config-path`/`--env-file` 적용. +- 안전: `from_env()` 의 legacy `.env` 로드(루트 `.env` 폴백)를 유지 → config/.env 부재 시에도 기존 루트 `.env` 로 동작(완전 하위호환). config/.env 존재 시 먼저 로드돼 우선(`override=False`). + +## Scope +- In Scope: + - `resolve_config_toml` + `DEFAULT_CONFIG_DIR` (REQ-048) + - `_load_dotenv(config_path, env_file)` config-dir 결정적 해석 + legacy 폴백 (REQ-010) + - 데몬 `main` argparse `--config-path`/`--env-file` + `from_config(toml_path, env_file)` + - 운영자 CLI `--config-path`/`--env-file` + `_resolve_instance` 통일 + - `.gitignore` `config/*` 허용목록, `config/.env.example` 신규 +- Out of Scope: + - 실제 `.env` → `config/.env` 물리 이동(운영 배포 절차 — 루트 `.env` 폴백으로 무중단) + - TOML/env 레이어링 정책(키 우선순위) 변경 — 기존 유지 + +## Documents +- Planning: [planning.md](./planning.md) +- Design: none (batch-lite with no architecture impact) +- Implementation: [verification.md](./verification.md) 에 구현 요약 포함 (batch-lite) +- Verification: [verification.md](./verification.md) + +## Notes +- Profile **batch-lite**: 국소 변경(config.py/daemon.py/cli.py + .gitignore + config 예제). 신규 런타임 흐름·계약 필드 없음(인자·해석 규칙 추가). `--config` 의미 확장은 인터페이스 영향이라 정식 change-req(REQ-048/010 CHG-20260609-01) 경유 → **Design Gate: no**. +- REQ-048/010 은 CHG-20260609-01 로 **Approved**(신규 AC 미구현). 본 batch release-candidate + confirm-req-implemented 로 **Implemented** 복귀. +- 발견 근거: 사용자 요청(trex-orb-v1 설정 관리 형태 정합). diff --git a/docs/batches/bat-026_20260609_config-dir-env-and-config-path/planning.md b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/planning.md new file mode 100644 index 0000000..8242805 --- /dev/null +++ b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/planning.md @@ -0,0 +1,32 @@ +# BAT-026 Planning + +- Status: confirmed +- Batch: BAT-026 +- Profile: batch-lite + +## Included REQ +- REQ-048 (CHG-20260609-01) — `--config` 이름 해석 + `--config-path` + `resolve_config_toml` +- REQ-010 (CHG-20260609-01) — config-dir `.env` 결정적 해석 + `--env-file` + `config/.env.example` + `.gitignore` + +## Delivery Plan +1. `config.py`: `DEFAULT_CONFIG_DIR` + `resolve_config_toml(name, config_path)`(슬래시 포함=명시 경로 하위호환). `_load_dotenv(config_path=None, env_file=None)` — `--env-file` 우선 → `/.env` → (config 컨텍스트 없으면) legacy `find_dotenv(usecwd=True)` 폴백. `from_config(toml_path, env_file=None)` 가 config-dir `.env` 선로드 후 `from_env()`. +2. `daemon.py main`: `--config-path`/`--env-file` argparse + `resolve_config_toml` + `from_config(path, env_file)`. +3. `cli.py`: `_add_common` 에 `--config-path`/`--env-file`; `_resolve_instance` 가 `resolve_config_toml` 경유; `main` 이 from_env 전 config-dir `.env` 선로드. +4. `.gitignore` `config/*` 허용목록(`*.example.toml`/`.env.example`), `config/.env.example` 신규. +5. 테스트: resolve_config_toml(이름/.toml/config-path/경로형), _load_dotenv(env-file/config-dir/override/부재/CWD무관), daemon·cli 인자. + +## Dependencies +- Internal: `from_env()` 의 legacy `.env` 로드 유지가 하위호환의 핵심. +- External: 없음. + +## Design Gate +- Design Required Before Implementation: no +- Reason: 국소 변경(3개 런타임 파일 + .gitignore + config 예제). 신규 계약 필드/런타임 흐름 없음. `--config` 의미 확장은 change-req(CHG-20260609-01)로 이미 정합. + +## Milestones +1. config.py/daemon.py/cli.py + .gitignore + config/.env.example 구현. +2. 단위(resolve/dotenv/인자) green + 전체 회귀 + ruff. + +## Risks +- R-1: config/.env 부재 시 키 미로드로 기동 실패. 대응: `from_env()` legacy 루트 `.env` 폴백 유지 → 무중단(완전 하위호환). +- R-2: `--config` 경로형 사용처 회귀. 대응: 슬래시 포함 값 명시 경로 폴백 + 회귀 테스트. diff --git a/docs/batches/bat-026_20260609_config-dir-env-and-config-path/verification.md b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/verification.md new file mode 100644 index 0000000..0278243 --- /dev/null +++ b/docs/batches/bat-026_20260609_config-dir-env-and-config-path/verification.md @@ -0,0 +1,45 @@ +# BAT-026 Verification + +- Status: approved +- Batch: BAT-026 +- Profile: batch-lite + +## Acceptance Mapping + +### REQ-048 (CHG-20260609-01) — `--config` 이름 해석 + `--config-path` +| AC | Evidence | +|----|----------| +| AC-7: `--config [--config-path ]` → `/.toml`(.toml 자동 부착) | `tests/runtime/test_config_resolution.py::test_resolve_name_default_dir` / `::test_resolve_appends_toml` / `::test_resolve_with_config_path` → PASS | +| AC-8: 슬래시 포함(경로형) 값은 명시 경로로 그대로 사용(하위호환) | `::test_resolve_path_form_passthrough` → PASS. 라이브: `trex-core universe --config config/test.toml` 정상(name 형태와 동일 결과) | +| AC-9: 운영자 CLI 도 `--config-path` 수용, `_resolve_instance` 동일 규칙 | `::test_cli_resolve_instance_honors_config_path` → PASS. 라이브: `--config test --config-path config` → instance=test 라우팅 | + +### REQ-010 (CHG-20260609-01) — config-dir `.env` + `--env-file` +| AC | Evidence | +|----|----------| +| AC-1(갱신): `/.env`(또는 `--env-file`) 로드 → dsn 반영 | `::test_dotenv_loaded_from_config_dir_cwd_independent` → PASS | +| AC-2: OS env 우선(`override=False`) | `::test_dotenv_os_env_takes_precedence` + 기존 `test_config_dotenv.py::test_os_env_takes_precedence_over_dotenv` → PASS | +| AC-3: `.env` 부재 시 회귀 없음 | `::test_dotenv_absent_is_noop` + 기존 `test_no_dotenv_uses_os_env_only` → PASS | +| AC-4: `.gitignore` `config/*` 무시 + `*.example.toml`/`.env.example` 추적 | `test_instance_config.py::test_gitignore_excludes_instance_toml`(갱신) → PASS | +| AC-6: CWD 무관 결정적 로드 | `::test_dotenv_loaded_from_config_dir_cwd_independent`(다른 CWD 로 chdir 후 config-dir `.env` 로드) → PASS | +| AC-7: `--env-file` 우선 | `::test_dotenv_env_file_wins` → PASS | + +## 하위호환 (R-1/R-2) +- `from_env()` 의 legacy `.env` 로드(루트 `.env` CWD 폴백) 유지 → config/.env 부재에도 기존 루트 `.env` 로 동작. 기존 `test_config_dotenv.py` 6건 전부 green(무회귀). +- 경로형 `--config` 무중단: 라이브 스모크에서 `--config config/test.toml` 와 `--config test --config-path config` 가 **byte 동일 스냅샷** 반환(instance=test). + +## 검증 명령/결과 +- 단위(config): `pytest tests/runtime/test_config_resolution.py test_config_dotenv.py test_instance_config.py -q` → **23 passed**. +- 전체 회귀: `pytest -q --deselect tests/adapters/broker/test_kis.py::test_sandbox_token_and_submit` → **375 passed, 35 skipped, 1 deselected**. +- lint: `ruff check`(config.py/daemon.py/cli.py + 신규/갱신 테스트) → All checks passed. +- 라이브 스모크: 실행 중 test 데몬에 `trex-core universe --config test --config-path config --json` 와 `--config config/test.toml --json` → 동일 응답(instance=test, 라우팅·.env 폴백 정상). + +## Baseline 문서 동기화 +- `config/.env.example` 신규(공유 인프라 키 + config-dir 해석 헤더). `docs/project-structure.md` 설정 계층 서술은 후속 정합(흐름 변화 없음 — 인자/해석 규칙 추가) — 구조 영향 없음(Design Gate: no). + +## 결과 요약 +- REQ-048 AC-7/8/9 + REQ-010 AC-1/2/3/4/6/7 전부 단위 PASS + 라이브 스모크 무회귀. +- **Ready for Release: true.** + +## 미검증/한계 +- 데몬 진입점(`daemon.py:main`)은 `# pragma: no cover`(프로세스 진입점) — `resolve_config_toml`/`from_config(env_file)` 단위 + CLI 라이브 스모크로 동등 경로 커버. +- 실제 `.env` → `config/.env` 물리 이동은 운영 배포 절차(루트 `.env` 폴백으로 무중단) — 본 batch 범위 밖. diff --git a/docs/batches/index.md b/docs/batches/index.md index 10911a2..88f28c2 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -33,3 +33,4 @@ | BAT-023 | standard | released | REQ-017 (CHG-20260608-02), REQ-019 (CHG-20260608-02), REQ-029 (CHG-20260608-01), REQ-030 (CHG-20260608-01, Approved→Implemented) | dcy-018 | [bat-023](./bat-023_20260608_ledger-cash-position-writeback-option-d/) | | BAT-024 | batch-lite | release-candidate | REQ-032 (CHG-20260609-01) | dcy-016 (issue #17) | [bat-024](./bat-024_20260609_operator-enter-forced-close-at/) | | BAT-025 | standard | release-candidate | REQ-002 (CHG-20260609-01), REQ-003 (CHG-20260609-01), REQ-006 (CHG-20260609-01), REQ-007 (CHG-20260609-01), REQ-009 (CHG-20260609-01) | dcy-019 (issue #18) | [bat-025](./bat-025_20260609_slot-entry-count-and-closed-reallocation/) | +| BAT-026 | batch-lite | release-candidate | REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01) | 없음 (trex-orb-v1 정합) | [bat-026](./bat-026_20260609_config-dir-env-and-config-path/) | diff --git a/docs/srs/Configuration/req-010_daemon-dotenv-loading.md b/docs/srs/Configuration/req-010_daemon-dotenv-loading.md index 79b0926..2addaa4 100644 --- a/docs/srs/Configuration/req-010_daemon-dotenv-loading.md +++ b/docs/srs/Configuration/req-010_daemon-dotenv-loading.md @@ -10,18 +10,21 @@ ## Requirement - `DaemonConfig.from_env()` 가 `os.environ` 을 읽기 전에 `.env`(있으면)를 로드한다. python-dotenv 를 의존성에 추가하고, `.env` 는 `.gitignore` 에 등록해 커밋되지 않게 하며, `.env.example` 를 참고용으로 제공한다. `.env` 가 없거나 dotenv 미설치여도 기존 동작(os.environ 단독)을 유지한다(회귀 없음). +- (CHG-20260609-01, trex-orb-v1 정합) `.env` 해석은 **config 디렉터리 기준 결정적**이다 — `--env-file ` 우선, 없으면 `/.env`(TOML 옆). CWD 의존 상향 탐색(`find_dotenv(usecwd=True)`)을 사용하지 않는다. `.env` 는 `config/.env` 로 공용화하고 `config/.env.example` 를 제공하며, `.gitignore` 는 `config/*` 를 무시하되 `*.example.toml`/`.env.example` 만 추적한다. `override=False`(실제 OS env 우선)는 유지한다. ## Acceptance Criteria -1. Given CWD 에 `.env`(`TREX_POSTGRESQL_DSN=...`)가 있고 해당 OS env 가 미설정, When `DaemonConfig.from_env()`, Then `.env` 값이 로드되어 `dsn` 에 반영된다. -2. Given OS env 와 `.env` 에 같은 키가 다른 값으로 존재, When `from_env()`, Then OS env 값이 우선한다(.env 가 덮어쓰지 않음). -3. Given `.env` 가 없음, When `from_env()`, Then 기존처럼 `os.environ` 만으로 동작한다(회귀 없음). -4. Given 저장소, When `.gitignore` 확인, Then `.env` 가 무시 대상이며 `.env.example` 은 추적된다. -5. Given `broker=paper` 이고 `.env`/OS env 에 `TREX_PAPER_FILL_AFTER_POLLS`/`TREX_PAPER_QTY_PER_POLL`/`TREX_PAPER_REJECT_SYMBOLS` 가 설정, When `DaemonConfig.from_env()` 후 `_build_broker`, Then paper 더블이 해당 시나리오(지연/부분/거부, REQ-011 AC-2)로 구성된다. 미설정이면 `PaperBroker()` 기본(즉시 전량 체결·거부 없음)과 byte-for-byte 동일하다(회귀 없음). +1. (CHG-20260609-01 갱신) Given `/.env`(또는 `--env-file`)에 `TREX_POSTGRESQL_DSN=...` 가 있고 해당 OS env 가 미설정, When 설정 로드, Then `.env` 값이 로드되어 `dsn` 에 반영된다. +2. Given OS env 와 `.env` 에 같은 키가 다른 값으로 존재, When 로드, Then OS env 값이 우선한다(.env 가 덮어쓰지 않음, `override=False`). +3. Given `.env` 가 없음, When 로드, Then 기존처럼 `os.environ` 만으로 동작한다(회귀 없음). +4. Given 저장소, When `.gitignore` 확인, Then `config/*` 가 무시 대상이며 `config/*.example.toml`·`config/.env.example` 은 추적된다. +5. Given `broker=paper` 이고 `.env`/OS env 에 `TREX_PAPER_FILL_AFTER_POLLS`/`TREX_PAPER_QTY_PER_POLL`/`TREX_PAPER_REJECT_SYMBOLS` 가 설정, When `DaemonConfig.from_env()` 후 `_build_broker`, Then paper 더블이 해당 시나리오(지연/부분/거부, REQ-011 AC-2)로 구성된다. 미설정이면 `PaperBroker()` 기본과 동일(회귀 없음). +6. (CHG-20260609-01) Given CWD 를 바꿔 실행, When 설정 로드, Then `/.env` 가 CWD 와 무관하게 결정적으로 로드된다(상향 탐색 없음). +7. (CHG-20260609-01) Given `--env-file `, When 로드, Then `/.env` 대신 그 파일이 로드된다(우선). ## Impacted Area -- Docs: 없음(baseline 구조/인터페이스/런타임 흐름 변경 없음 — minor-change) -- Modules: pyproject.toml(python-dotenv), trex_core/runtime/config.py(load_dotenv, PaperBrokerConfig), trex_core/runtime/daemon.py(_build_broker paper 주입), .gitignore, .env.example(신규 + paper 튜닝 키) -- Tests: tests/runtime/test_config_dotenv.py (로드/우선순위/부재 회귀, paper 튜닝 기본/파싱) +- Docs: (CHG-20260609-01) `config/.env.example`(신규, 공유 인프라 키), `.env`→`config/.env` 이동 +- Modules: pyproject.toml(python-dotenv), trex_core/runtime/config.py(load_dotenv→`_load_dotenv(config_path, env_file)` config-dir 해석, PaperBrokerConfig), trex_core/runtime/daemon.py(_build_broker paper 주입, `--env-file`), cli.py(`--env-file`), .gitignore(config/* 허용목록), .env.example(paper 튜닝 키) +- Tests: tests/runtime/test_config_dotenv.py (로드/우선순위/부재 회귀, paper 튜닝 기본/파싱, config-dir·`--env-file`·CWD 무관) ## Notes - minor-change fast path: 단일 저위험 Configuration REQ, 구조/인터페이스/런타임 흐름 영향 없음(기동 시 env 소스 추가뿐). batch-lite 로 처리, design 불요. @@ -235,3 +238,81 @@ #### Validation Plan - BAT-009 verification evidence 재참조로 충분. + +### CHG-20260609-01 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- (change-req, trex-orb-v1 설정 관리 정합) `.env` 해석을 **config 디렉터리 기준 결정적**으로 변경한다 — `--env-file ` 가 우선, 없으면 **`/.env`**(TOML 옆). 기존 `find_dotenv(usecwd=True)`(CWD 의존 상향 탐색)를 제거해 실행 위치와 무관하게 동일 `.env` 를 로드한다. `.env` 를 repo 루트 → **`config/.env`** 로 이동(공용화), `config/.env.example` 신설, `.gitignore` 를 `config/*` 무시 + `*.example.toml`/`.env.example` 만 추적하는 형태로 전환. `override=False`(실제 OS env 우선)는 유지. `config_dir` 는 REQ-048 CHG-20260609-01 의 `--config-path` 로 제공된다. + +#### Intent + +- 어디서 실행하든 동일한 **공유 인프라 `.env`**(DSN/RabbitMQ/KIS/portfolio-hub 등)를 결정적으로 로드한다. 다중 인스턴스가 같은 broker/DSN/hub 를 공유하도록 `.env` 를 인스턴스 TOML 과 분리해 config 디렉터리에 공용화(12-factor). + +#### Acceptance Criteria Delta + +- Added: + - AC-6: `.env` 는 `/.env`(TOML 옆)에서 **CWD 와 무관하게** 결정적으로 로드된다. dotenv 미설치/`.env` 부재 시 no-op(회귀 없음). + - AC-7: `--env-file ` 가 주어지면 `/.env` 대신 그 파일을 로드한다(우선). + - AC-8: `.gitignore` 는 `config/*` 를 무시하되 `config/*.example.toml` 과 `config/.env.example` 은 추적한다(실제 `.env`/인스턴스 TOML 비커밋). +- Updated: + - AC-1: "CWD 의 `.env`" → "`/.env`"(또는 `--env-file`) 로 로드 소스 변경. +- Removed: none + +#### Impacted Area Delta + +- Modules: `trex_core/runtime/config.py`(`_load_dotenv(config_path, env_file)` — config-dir 결정적 해석, `find_dotenv(usecwd=True)` 제거), `trex_core/runtime/daemon.py`·`cli.py`(`--env-file` 인자 전달) +- Docs: `config/.env.example`(신규, 공유 인프라 키), `.gitignore`(config/* 허용목록), `.env`(루트→`config/.env` 이동) +- Tests: `tests/runtime/test_config_dotenv.py`(config-dir 로드/`--env-file` 우선/CWD 무관/`override=False` 회귀/부재 no-op) + +#### Delivery Trace Delta + +- Batches: 신규 batch-lite(REQ-048 CHG-20260609-01 과 동일 배치) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes (신규 batch 에서 AC-6/7/8 + AC-1 변경 검증) +- Existing Implementation Invalidated: yes (`.env` 해석 위치가 CWD → config-dir 로 변경되어 기존 루트 `.env` 의존 동작이 바뀜 — 운영 배포 시 `.env` 이동 필요) +- Status Recommendation: Implemented → Approved (신규/변경 AC 미구현 — batch 재검증 후 Implemented 복귀) +- Follow-up Action: draft-batch(batch-lite) → run-batch-implementation → verification → confirm-req-implemented + +#### Validation Plan + +- 신규 BAT: `/.env` 결정적 로드(CWD 변경해도 동일), `--env-file` 우선, OS env 우선(`override=False`) 회귀, `.env` 부재 no-op 단위. `.gitignore` config/* 허용목록 가드. + +### CHG-20260609-02 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- Status 변경: Approved → Implemented. BAT-026 release-candidate verification evidence 기반(CHG-20260609-01 AC-1/2/3/4/6/7). + +#### Acceptance Criteria Delta + +- Added/Updated/Removed: 없음 + +#### Impacted Area Delta + +- Docs/Modules/Tests: 없음 + +#### Delivery Trace Delta + +- Batches: BAT-026 (release-candidate) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: no (BAT-026 에서 완료) +- Existing Implementation Invalidated: no +- Status Recommendation: Implemented +- Follow-up Action: 운영 배포 시 `.env`→`config/.env` 이동(루트 `.env` 폴백으로 무중단). release 단계 별도 진행. + +#### Validation Plan + +- tests/runtime/test_config_resolution.py(config-dir `.env`/`--env-file`/CWD 무관/override) + test_config_dotenv.py 6건 무회귀 + `.gitignore` 가드. 전체 375 passed, ruff clean. diff --git a/docs/srs/Configuration/req-048_per-instance-toml-config.md b/docs/srs/Configuration/req-048_per-instance-toml-config.md index d9b32b0..d51e9aa 100644 --- a/docs/srs/Configuration/req-048_per-instance-toml-config.md +++ b/docs/srs/Configuration/req-048_per-instance-toml-config.md @@ -14,6 +14,7 @@ - `config.py` 에 `DaemonInstanceConfig` dataclass(`instance_key: str`, `portfolio_hub_token: str`)와 `from_toml(path)` 로더를 추가한다(stdlib `tomllib` 사용). - `DaemonConfig` 구성 시 `--config .toml` 의 TOML 값을 `from_env()` 결과와 병합하는 팩토리(예: `from_config(toml_path)`)를 추가한다. `portfolio_hub_token` 은 TOML 값을 사용해 `PortfolioHubConfig.token` 에 주입한다. - 데몬 기동 진입점은 `--config` 를 **필수 인자**로 받는다. 인자 없이 기동하면 오류 메시지를 출력하고 비정상 종료(exit code ≠ 0)한다. 기본값 fallback 없음(OQ-2 Answered). +- (CHG-20260609-01, trex-orb-v1 정합) `--config` 는 **이름**으로 받고 `--config-path `(기본 `./config`)로 config 디렉터리를 분리한다. `resolve_config_toml(name, config_path)` 가 `/.toml` 로 해석하며 `.toml` 을 자동 부착한다. **하위호환**: 슬래시 포함(경로형) `--config` 값은 명시 경로로 그대로 사용한다. 운영자 CLI(`trex-core`)도 `--config-path` 를 동일 규칙으로 수용한다. - `instance_key` 와 `portfolio_hub_token` 은 `.env`/OS env 에서 읽지 않는다. TOML 미존재/필수 필드 누락 시 명확한 오류로 종료한다. - `.gitignore` 에 인스턴스 TOML 경로 패턴(예: `*.toml` 또는 `config/*.toml`)을 추가해 설정 파일이 커밋되지 않게 한다. 토큰/instance_key 는 로그에 평문 출력하지 않는다(NFR-4). @@ -25,14 +26,18 @@ 4. Given TOML 에 필수 필드(`instance_key`) 누락, When `from_toml`/`from_config`, Then 명확한 오류로 실패한다(조용한 기본값 금지). 5. Given 저장소, When `.gitignore` 확인, Then 인스턴스 TOML 경로 패턴이 무시 대상이다. 6. Given 데몬 기동 로그, When 출력 확인, Then `portfolio_hub_token` 평문이 로그에 노출되지 않는다. +7. (CHG-20260609-01) Given `--config [--config-path ]`, When `resolve_config_toml`, Then `/.toml` 로 해석되고 `.toml` 이 없으면 자동 부착된다. +8. (CHG-20260609-01) Given 슬래시 포함(경로형) `--config` 값, When 해석, Then `--config-path` 와 무관하게 명시 경로로 그대로 사용된다(하위호환). +9. (CHG-20260609-01) Given 운영자 CLI 가 `--config-path` 와 함께 호출, When `_resolve_instance`, Then 동일 `resolve_config_toml` 규칙으로 TOML 을 찾아 instance_key 를 해석한다. ## Impacted Area - Docs: - `docs/project-structure.md` — 설정 파일 계층(TOML + env) (REQ-051 처리) - Modules: - - `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config` 병합 - - 데몬 진입점(`trex_core/runtime/daemon.py:main` 또는 `__main__`) — `--config` 필수 파싱(argparse) + - `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config` 병합, (CHG-20260609-01) `resolve_config_toml`/`DEFAULT_CONFIG_DIR` + - 데몬 진입점(`trex_core/runtime/daemon.py:main` 또는 `__main__`) — `--config` 필수 파싱(argparse), (CHG-20260609-01) `--config-path` + - 운영자 CLI `trex_core/runtime/cli.py` — (CHG-20260609-01) `--config-path` + `_resolve_instance` 통일 - `.gitignore` — 인스턴스 TOML 패턴 - Tests: - `tests/runtime/` — TOML 로더 파싱/필수 인자/토큰 주입/누락 오류 @@ -186,3 +191,81 @@ #### Validation Plan - BAT-018: from_toml 기본값(KST/00:00) + TOML 값 from_config 전파 단위. + +### CHG-20260609-01 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- (change-req, trex-orb-v1 설정 관리 정합) `--config` 를 **경로 대신 이름**으로 받고 `--config-path `(기본 `./config`)로 config 디렉터리를 분리한다. `resolve_config_toml(name, config_path)` 추가 — `/.toml`(`.toml` 자동 부착). **하위호환**: 슬래시 포함(경로형) `--config` 값은 명시 경로로 그대로 사용(기존 운영 기동 스크립트 무중단). 운영자 CLI(`trex-core`)에도 `--config-path` 를 적용해 인스턴스 라우팅을 동일 규칙으로 통일. 데몬·CLI 진입점 argparse 갱신. + +#### Intent + +- trex-orb-v1 과 동일한 config 디렉터리 기반 설정 관리로 통일 — CWD 독립·이름 기반 인스턴스 선택. 공유 `.env`(REQ-010 CHG-20260609-01)의 해석 기준 디렉터리(`config_dir`)를 `--config-path` 로 제공한다. + +#### Acceptance Criteria Delta + +- Added: + - AC-7: `--config [--config-path ]` → `/.toml` 로 해석되고 `.toml` 확장자가 없으면 자동 부착된다(`resolve_config_toml`). + - AC-8: 슬래시를 포함한(경로형) `--config` 값은 `--config-path` 와 무관하게 **명시 경로**로 그대로 사용된다(하위호환 — 기존 `--config config/test.toml` 무중단). + - AC-9: 운영자 CLI(`trex-core`)도 `--config-path` 를 수용하며, `_resolve_instance` 가 동일 `resolve_config_toml` 규칙으로 TOML 을 찾는다. +- Updated: + - AC-2(필수 인자)는 유지 — `--config` 는 여전히 필수다(이름 또는 경로). +- Removed: none + +#### Impacted Area Delta + +- Modules: `trex_core/runtime/config.py`(`resolve_config_toml`, `DEFAULT_CONFIG_DIR`), `trex_core/runtime/daemon.py`(`main` argparse `--config-path`), `trex_core/runtime/cli.py`(`--config-path` + `_resolve_instance` 통일) +- Docs: `docs/project-structure.md`(설정 파일 계층 — config 디렉터리/이름 해석), `config/instance.example.toml` +- Tests: `tests/runtime/`(resolve_config_toml: 이름/.toml 부착/config-path/경로형 폴백, 데몬·CLI `--config-path` 라우팅) + +#### Delivery Trace Delta + +- Batches: 신규 batch-lite(REQ-010 CHG-20260609-01 과 동일 배치) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes (신규 batch 에서 AC-7/8/9 검증) +- Existing Implementation Invalidated: no (경로형 `--config` 는 하위호환 유지, 이름 해석은 additive) — 단 신규 AC 미구현 +- Status Recommendation: Implemented → Approved (신규 AC-7/8/9 미구현 — batch 재검증 후 Implemented 복귀) +- Follow-up Action: draft-batch(batch-lite) → run-batch-implementation → verification → confirm-req-implemented + +#### Validation Plan + +- 신규 BAT: `resolve_config_toml("test")`→`config/test.toml`, `("test.toml")`→`config/test.toml`, `("prod","/etc/trex")`→`/etc/trex/prod.toml`, 경로형(`config/test.toml`)→그대로 단위. 데몬·CLI `--config-path` 기동/라우팅 단위. 기존 경로형 `--config` 회귀. + +### CHG-20260609-02 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- Status 변경: Approved → Implemented. BAT-026 release-candidate verification evidence 기반(CHG-20260609-01 AC-7/8/9). + +#### Acceptance Criteria Delta + +- Added/Updated/Removed: 없음 + +#### Impacted Area Delta + +- Docs/Modules/Tests: 없음 + +#### Delivery Trace Delta + +- Batches: BAT-026 (release-candidate) +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: no (BAT-026 에서 완료) +- Existing Implementation Invalidated: no +- Status Recommendation: Implemented +- Follow-up Action: release 단계 별도 진행 + +#### Validation Plan + +- tests/runtime/test_config_resolution.py(resolve_config_toml/경로형 폴백/CLI `--config-path` 라우팅) + 라이브 스모크(name·path 형태 동일 결과). 전체 375 passed, ruff clean. diff --git a/docs/srs/index.md b/docs/srs/index.md index 66dede9..e0ff7c7 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -139,8 +139,8 @@ ## Recent Change Log Summary +- 2026-06-09 :: REQ-048/010 change-req→Implemented (CHG-20260609-01/02, trex-orb-v1 설정 관리 정합, BAT-026): config 디렉터리 기반 설정 관리 도입 — `--config` 를 이름으로 해석 + `--config-path `(기본 `./config`) + `resolve_config_toml`(슬래시 포함 값은 하위호환 명시 경로)(REQ-048), `.env` 를 config-dir 결정적 해석(`--env-file` 우선, 없으면 `/.env`, CWD 무관)으로 변경 + `config/.env.example` + `.gitignore` `config/*` 허용목록(REQ-010). 운영자 CLI 도 `--config-path`/`--env-file` 동일 적용. `from_env()` legacy 루트 `.env` 폴백 유지로 완전 하위호환(`.env` 이동은 운영 배포 시 무중단). batch-lite(BAT-026) 단위 23 + 전체 375 passed, ruff clean, 라이브 스모크(name·path 동일 결과) → 둘 다 **Implemented**. (change-req/confirm-req-implemented) - 2026-06-09 :: REQ-002/003/006/007/009 change-req (CHG-20260609-01, dcy-019/issue #18, BAT-025): slot-scoped `entry_count` 추가 + closed 슬롯 직접 재할당. daily_slot/`Slot` 에 `entry_count`(기본 0) 필드 + `EXPECTED_SLOT_FIELDS` + 멱등 마이그레이션 008(REQ-002/006), 상태 머신 `closed→pending_buy` 신규 전이(8→9 base, ALLOWED_TRANSITIONS 14→15)(REQ-002), `_apply_transition_fields` closed→pending_buy clean-slate + 모든 `→pending_buy` 시 entry_count +1(REQ-002), `find_allocatable_slot`(idle∪closed, `entry_count→updated_at→slot_id`) selector + in-memory 동등(REQ-006), `_admit_buy` find_idle_slot→find_allocatable_slot + from_state=slot.status(REQ-007), 테스트 계약 정합(REQ-003), baseline 2문서(data-model/runtime-flows) 정합(REQ-009). 기존 `daily_symbol_entries`(symbol-scoped) 무관·미변경. 전부 change-req(신규 REQ 없음, OQ-1/2/3). 단위 366 passed(+KIS sandbox 1건은 live .env URL 로 사전 환경 실패·무관), ruff clean. PG 게이트 마이그레이션/드리프트는 라이브 runtime DSN 미사용(파괴적 truncate 회피)·test DSN 자동 검증. 전부 **Implemented** 유지. (change-req) - 2026-06-09 :: REQ-032 change-req→Implemented (CHG-20260609-01/02, issue #17, BAT-024): `trex-core enter` 에 `--forced-close-at ` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약 REQ-007/021·도메인 REQ-066·forced-close 루프 REQ-037 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 제출 전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택. AC-5/6 신규. batch-lite(BAT-024) 단위 검증 → **Implemented**. 단위 353 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (dcy-018, BAT-023): ledger write 응답의 권위 cash/quantity 를 portfolio 스냅샷에 write-back(option D 활성화) — 매수/매도 직후 "수량 live·현금 stale" 비대칭 제거. `record_trade`→`LedgerWriteOutcome{result, *_after}`+어댑터 `cash_balance_after`/`asset_quantity_after` 파싱(new+duplicate)(REQ-017), `LedgerWriter` SnapshotStore write-back best-effort+`pending_cash_delta`(REQ-019), 추정 현금 오버레이(`cash_estimated` 라벨)+슬롯 오버레이 유지(REQ-029, OQ-1 B/OQ-4 i), option D 활성화로 REQ-030 Approved→Implemented. 전부 change-req(신규 REQ 없음, OQ-3 a). portfolio-hub#8(`*_after`) 동작 중. 단위 350 passed, 인프라 게이트 385 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented) -- 2026-06-08 :: REQ-034/021/037/038/039/032/042 change-req (dcy-017, BAT-021): 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태를 가시화(issue #12). snapshot RPC 응답에 holding 슬롯 워치독 상태(expired/last_seen) read-only 동봉(best-effort) + `WatchdogStore.last_seen`(REQ-034/021), `trex-core slots` WATCHDOG-OWNED 표시(REQ-032), safety-sell 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그+in-process 카운터(REQ-037/038/039), baseline 2문서 정합(REQ-042). best-effort(청산 무차단), Redis 미채택. 전부 change-req(신규 REQ 없음, OQ-3). 단위 335 passed, 인프라 게이트 369 passed, ruff clean. (change-req/confirm-req-implemented) diff --git a/tests/runtime/test_config_resolution.py b/tests/runtime/test_config_resolution.py new file mode 100644 index 0000000..b27ee96 --- /dev/null +++ b/tests/runtime/test_config_resolution.py @@ -0,0 +1,87 @@ +"""config-dir resolution + config-dir/.env loading (REQ-048/010 CHG-20260609-01).""" +from __future__ import annotations + +import os + +import pytest + +from trex_core.runtime.config import _load_dotenv, resolve_config_toml + +_K = "TREX_TEST_RESOLVE_KEY" + + +@pytest.fixture(autouse=True) +def _clean(monkeypatch): + monkeypatch.delenv(_K, raising=False) + + +# --- resolve_config_toml (REQ-048 AC-7/8) ---------------------------------- # +def test_resolve_name_default_dir(): + assert resolve_config_toml("test") == os.path.join("config", "test.toml") + + +def test_resolve_appends_toml(): + assert resolve_config_toml("test.toml") == os.path.join("config", "test.toml") + + +def test_resolve_with_config_path(): + assert resolve_config_toml("prod", "/etc/trex") == os.path.join("/etc/trex", "prod.toml") + + +def test_resolve_path_form_passthrough(): + # AC-8: a slash-bearing value is an explicit path (backward-compat). + assert resolve_config_toml("config/test.toml") == "config/test.toml" + assert resolve_config_toml("config/test.toml", "/etc/trex") == "config/test.toml" + + +# --- _load_dotenv config-dir resolution (REQ-010 AC-6/7) ------------------- # +def test_dotenv_loaded_from_config_dir_cwd_independent(tmp_path, monkeypatch): + cfgdir = tmp_path / "cfg" + cfgdir.mkdir() + (cfgdir / ".env").write_text(f"{_K}=from_config_dir\n") + toml = str(cfgdir / "inst.toml") + # chdir somewhere with no .env to prove CWD-independence. + other = tmp_path / "elsewhere" + other.mkdir() + monkeypatch.chdir(other) + _load_dotenv(toml) + assert os.environ[_K] == "from_config_dir" + + +def test_dotenv_env_file_wins(tmp_path, monkeypatch): + cfgdir = tmp_path / "cfg" + cfgdir.mkdir() + (cfgdir / ".env").write_text(f"{_K}=from_config_dir\n") + explicit = tmp_path / "explicit.env" + explicit.write_text(f"{_K}=from_env_file\n") + monkeypatch.chdir(tmp_path) + _load_dotenv(str(cfgdir / "inst.toml"), env_file=str(explicit)) + assert os.environ[_K] == "from_env_file" + + +def test_dotenv_os_env_takes_precedence(tmp_path, monkeypatch): + cfgdir = tmp_path / "cfg" + cfgdir.mkdir() + (cfgdir / ".env").write_text(f"{_K}=from_config_dir\n") + monkeypatch.setenv(_K, "from_os_env") + _load_dotenv(str(cfgdir / "inst.toml")) + assert os.environ[_K] == "from_os_env" # override=False + + +def test_dotenv_absent_is_noop(tmp_path): + # config dir has no .env -> no crash, key stays unset. + _load_dotenv(str(tmp_path / "inst.toml")) + assert _K not in os.environ + + +# --- CLI --config-path instance routing (REQ-048 AC-9) --------------------- # +def test_cli_resolve_instance_honors_config_path(tmp_path): + from types import SimpleNamespace + + from trex_core.runtime.cli import _resolve_instance + + cfgdir = tmp_path / "cfg" + cfgdir.mkdir() + (cfgdir / "inst.toml").write_text('instance_key = "inst-xyz"\n') + args = SimpleNamespace(instance=None, config="inst", config_path=str(cfgdir)) + assert _resolve_instance(args) == "inst-xyz" diff --git a/tests/runtime/test_instance_config.py b/tests/runtime/test_instance_config.py index aa836a8..b92ea50 100644 --- a/tests/runtime/test_instance_config.py +++ b/tests/runtime/test_instance_config.py @@ -83,7 +83,9 @@ def test_config_required_cli_exits_nonzero(): def test_gitignore_excludes_instance_toml(): - """AC-5: .gitignore excludes instance TOML but keeps the example.""" + """AC-5 (CHG-20260609-01): .gitignore ignores config/* but keeps example + templates (*.example.toml covers instance.example.toml) and .env.example.""" gi = (_REPO / ".gitignore").read_text() - assert "/config/*.toml" in gi - assert "!/config/instance.example.toml" in gi + assert "/config/*" in gi + assert "!/config/*.example.toml" in gi + assert "!/config/.env.example" in gi diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 7ea75a1..c1045a1 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -30,7 +30,12 @@ from zoneinfo import ZoneInfo from trex_core.adapters.messaging import topology from trex_core.adapters.portfolio.pause_store import PostgreSQLPauseStore -from trex_core.runtime.config import DaemonConfig, DaemonInstanceConfig +from trex_core.runtime.config import ( + DaemonConfig, + DaemonInstanceConfig, + _load_dotenv, + resolve_config_toml, +) _DEFAULT_TIMEOUT = 10.0 @@ -42,7 +47,8 @@ def _resolve_instance(args) -> str: if getattr(args, "instance", None): return args.instance if getattr(args, "config", None): - return DaemonInstanceConfig.from_toml(args.config).instance_key + toml = resolve_config_toml(args.config, getattr(args, "config_path", None)) + return DaemonInstanceConfig.from_toml(toml).instance_key return topology.DEFAULT_INSTANCE_KEY @@ -248,8 +254,12 @@ def _fail(message: str, as_json: bool) -> None: # arg parsing # --------------------------------------------------------------------------- # def _add_common(p) -> None: - p.add_argument("--config", help="per-instance TOML (canonical instance routing)") + p.add_argument("--config", help="per-instance TOML name (resolved within --config-path) or path") + p.add_argument("--config-path", dest="config_path", default=None, + help="config directory (default ./config) for --config + .env") p.add_argument("--instance", help="instance key (shortcut; overrides --config)") + p.add_argument("--env-file", dest="env_file", default=None, + help="explicit .env path; overrides /.env") p.add_argument("--json", action="store_true", help="machine-readable JSON output") p.add_argument("--timeout", type=float, default=_DEFAULT_TIMEOUT) @@ -313,6 +323,15 @@ def main(argv: list[str] | None = None, *, rpc=None, confirm=None) -> int: correlation_id, timeout) -> dict`, `confirm(prompt) -> bool`. """ args = _build_parser().parse_args(argv) + # REQ-010 CHG-20260609-01: load the config-dir .env (or --env-file) before + # reading env. from_env() also runs the legacy root-.env fallback; the + # config-dir load wins (loaded first, override=False). + _toml = ( + resolve_config_toml(args.config, getattr(args, "config_path", None)) + if getattr(args, "config", None) + else None + ) + _load_dotenv(_toml, getattr(args, "env_file", None)) cfg = DaemonConfig.from_env() confirm = confirm or _default_confirm diff --git a/trex_core/runtime/config.py b/trex_core/runtime/config.py index 055bb1a..03c84c2 100644 --- a/trex_core/runtime/config.py +++ b/trex_core/runtime/config.py @@ -6,17 +6,57 @@ from dataclasses import dataclass, replace from typing import Optional -def _load_dotenv() -> None: - """Best-effort load of a .env file into os.environ (REQ-010). +DEFAULT_CONFIG_DIR = "config" - Real OS environment variables take precedence over .env (override=False), - matching 12-factor expectations. If python-dotenv is not installed or no - .env exists, this is a no-op (no regression). + +def resolve_config_toml(name: str, config_path: Optional[str] = None) -> str: + """Resolve `--config ` within a config directory (REQ-048 CHG-20260609-01). + + config_dir = `--config-path` if given, else `./config`. `` selects the + TOML inside it; a missing `.toml` extension is appended. + + Backward-compat: if `name` contains a path separator (e.g. `config/test.toml`), + it is treated as an explicit path and used as-is so existing ops scripts that + pass a path keep working. + + resolve_config_toml("test") -> config/test.toml + resolve_config_toml("test.toml") -> config/test.toml + resolve_config_toml("prod", "/etc/trex") -> /etc/trex/prod.toml + resolve_config_toml("config/test.toml") -> config/test.toml (path-form) + """ + if os.sep in name or (os.altsep and os.altsep in name): + return name # explicit path (backward-compat) + config_dir = config_path or DEFAULT_CONFIG_DIR + filename = name if name.endswith(".toml") else f"{name}.toml" + return os.path.join(config_dir, filename) + + +def _load_dotenv(config_path: Optional[str] = None, env_file: Optional[str] = None) -> None: + """Best-effort load of a .env file into os.environ (REQ-010 / CHG-20260609-01). + + Resolution (trex-orb-v1 aligned, deterministic & CWD-independent given a + config_path): `env_file` (explicit) wins, else `/.env` next to the + TOML. With neither (standalone `from_env()`), fall back to legacy CWD-relative + `find_dotenv` so existing root `.env` setups keep working (backward-compat). + + Real OS env vars always take precedence (`override=False`). python-dotenv + missing or the resolved file absent => no-op (no regression). """ try: - from dotenv import find_dotenv, load_dotenv + from dotenv import load_dotenv except ImportError: # pragma: no cover - optional at runtime return + if env_file: + if os.path.isfile(env_file): + load_dotenv(env_file, override=False) + return + if config_path: + path = os.path.join(os.path.dirname(os.path.abspath(config_path)), ".env") + if os.path.isfile(path): + load_dotenv(path, override=False) + return + # No config context: legacy CWD-relative discovery (backward-compat fallback). + from dotenv import find_dotenv load_dotenv(find_dotenv(usecwd=True), override=False) @@ -252,13 +292,18 @@ class DaemonConfig: ) @staticmethod - def from_config(toml_path: str) -> "DaemonConfig": + def from_config(toml_path: str, env_file: Optional[str] = None) -> "DaemonConfig": """Build config from env + a per-instance TOML file (REQ-048 / BAT-013). `instance_key` and `portfolio_hub_token` come from TOML (not .env). TOML `portfolio_hub_token` overrides the (env-derived) PortfolioHubConfig token when a portfolio-hub base_url is configured. + + REQ-010 CHG-20260609-01: the config-dir `.env` (next to the TOML, or + `--env-file`) is loaded first so it wins over the legacy root `.env` + fallback loaded inside `from_env()` (both `override=False`). """ + _load_dotenv(toml_path, env_file) base = DaemonConfig.from_env() inst = DaemonInstanceConfig.from_toml(toml_path) pf = base.portfolio_hub diff --git a/trex_core/runtime/daemon.py b/trex_core/runtime/daemon.py index 8e3a878..0af40fb 100644 --- a/trex_core/runtime/daemon.py +++ b/trex_core/runtime/daemon.py @@ -66,7 +66,7 @@ from trex_core.application.preallocation_scheduler import DailyPreallocationSche from trex_core.application.snapshot_scheduler import SnapshotScheduler from trex_core.application.watchdog import WatchdogStore from trex_core.domain.slot import SlotService -from trex_core.runtime.config import DaemonConfig +from trex_core.runtime.config import DaemonConfig, resolve_config_toml _log = logging.getLogger(__name__) @@ -375,11 +375,21 @@ def main() -> None: # pragma: no cover - process entrypoint parser = argparse.ArgumentParser(prog="trex-core-daemon") parser.add_argument( "--config", required=True, - help="per-instance TOML config path (instance_key, portfolio_hub_token). " - "Required — no default (REQ-048 / dcy-009 OQ-2).", + help="per-instance TOML name (resolved within --config-path) or explicit " + "path. Required — no default (REQ-048 / dcy-009 OQ-2).", + ) + parser.add_argument( + "--config-path", default=None, + help="config directory (default ./config) for --config name resolution " + "and the matching .env (REQ-048/010 CHG-20260609-01).", + ) + parser.add_argument( + "--env-file", default=None, + help="explicit .env path; overrides /.env (REQ-010 CHG-20260609-01).", ) args = parser.parse_args() - config = DaemonConfig.from_config(args.config) + toml_path = resolve_config_toml(args.config, args.config_path) + config = DaemonConfig.from_config(toml_path, env_file=args.env_file) Daemon(config).run()