From 55ec9204f30d3cc21e622a4c7e63e5e97056ff87 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:21:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?docs(PLAN55):=20devbase=20up=20=E3=81=AE?= =?UTF-8?q?=E6=A9=9F=E5=AF=86=E3=81=AE=E6=B3=A8=E5=85=A5=E3=82=92=201=20?= =?UTF-8?q?=E5=9B=9E=E3=81=AB=E3=81=99=E3=82=8B=E8=A6=81=E6=B1=82=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=A8=E8=A8=AD=E8=A8=88=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN55_up-single-injection-design.md | 228 ++++++++++++++++++++ issues/PLAN55_up-single-injection.md | 159 ++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 issues/PLAN55_up-single-injection-design.md create mode 100644 issues/PLAN55_up-single-injection.md diff --git a/issues/PLAN55_up-single-injection-design.md b/issues/PLAN55_up-single-injection-design.md new file mode 100644 index 00000000..21059763 --- /dev/null +++ b/issues/PLAN55_up-single-injection-design.md @@ -0,0 +1,228 @@ +# #168: `devbase up` の機密の注入を 1 回にし、サーバ backend の往復を設計の想定へ収める(設計) + +要求と受け入れ条件は `issues/PLAN55_up-single-injection.md` にある。この文書は「どう作るか」だけを扱う。 + +作るものは 1 つである。**1 回のライフサイクル操作の間、`SecretStore` を 1 つだけ持ち回る** +置き場(`runtime.store_for()` / `runtime.release_store()`)。注入の回数そのものは変えない。 +同じ `SecretStore` なら 2 度目の解決は控え(`_seen`)から返り、サーバへは行かない。 +これで `up` 1 回の往復は認証 1 回 + 参照ごとに 1 回になる(決定 1)。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | `devbase up`(3 経路)が、backend `openbao` でも認証 1 回 + 参照ごとに GET 1 回で起動する | 開発者(意識せずに使う) | +| F2 | `_ensure_env_files` の存在判定がサーバへ問い合わせない | 開発者(同上) | +| F3 | `up` 1 回の往復回数がテストで固定される | 保守する人 | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| `env/runtime.py` `store_for(root)`(足す) | プロセス内で持ち回る `SecretStore` を返す。無ければ作る。`root` が変われば作り直す | +| `env/runtime.py` `release_store()`(足す) | 持ち回っている `SecretStore` を捨てる。次の `store_for` は作り直す | +| `env/runtime.py` `resolve()` / `inject()` / `child_env()`(変える) | `store` 引数が `None` のとき `SecretStore(root)` ではなく `store_for(root)` を使う。引数の形は変えない | +| `commands/container.py` `_ensure_env_files()`(変える) | `SecretStore(devbase_root)` を `runtime.store_for(devbase_root)` に置き換える | +| `commands/container.py` `_dispatch_lifecycle()`(変える) | `finally` で `docker_context.reset()` に並べて `runtime.release_store()` を呼ぶ | +| `cli.py` `_load_secret_env()`(変えない) | dispatch 前の注入はそのまま。作った `SecretStore` が `store_for` の控えになる | +| `tests/env/test_runtime_store.py`(新設) | `store_for` / `release_store` の振る舞い(同一性・`root` 変更・解放後の作り直し) | +| `tests/cli/test_up_roundtrips.py`(新設) | 3 経路の `up` を `FakeOpenBao` で走らせ、認証と GET の回数を固定する | +| `docs/specifications/secret-backend.md`「OpenBao との契約」(`plan-to-spec` で変える) | 「`devbase up` 1 回あたり」の文を、持ち回りの規則とともに確定仕様にする | + +構成要素の関係: + +```mermaid +graph TD + subgraph cli [cli.py] + LOAD[_load_secret_env] + end + subgraph container [commands/container.py] + DL[_dispatch_lifecycle] + INJ[_inject_secrets] + ENS[_ensure_env_files] + DEP[_run_deploy_pipeline] + end + subgraph runtime [env/runtime.py] + SF[store_for] + RS[release_store] + RES[resolve / inject / child_env] + end + ST[(SecretStore
OpenBaoBackend._seen)] + SRV[(OpenBao)] + LOAD --> RES + DL --> INJ + DL -->|finally| RS + INJ --> RES + ENS --> SF + DEP --> INJ + RES --> SF + SF --> ST + RS -.捨てる.-> ST + ST -->|参照ごとに 1 回| SRV +``` + +## 構造 + +```mermaid +classDiagram + class runtime { + -_store: SecretStore | None + -_store_root: Path | None + +store_for(root: Path) SecretStore + +release_store() None + +resolve(root, project, store=None) SecretEnv + +inject(root, project, environ=None, store=None) SecretEnv + +child_env(root, project, base=None, store=None) dict + } + class SecretStore { + +root: Path + +exists(ref) bool + +load(ref) dict + } + class OpenBaoBackend { + -_seen: dict~SecretRef, _Basis~ + +fetch(ref) dict + +load(ref) dict + +exists(ref) bool + } + runtime --> SecretStore : 持ち回る + SecretStore --> OpenBaoBackend : backend が openbao +``` + +`OpenBaoBackend` は変えない。`_seen` は既にあり、同じインスタンスの中では `exists` → `load` +で 2 度取りに行かない。これは `docs/specifications/secret-backend.md`「OpenBao との契約」が +定める性質である。この設計は**インスタンスの寿命を延ばす**ことで、その性質を CLI 全体へ +広げる。 + +## 処理の流れ + +`devbase up web` を `projects/api` の中から打ったとき: + +```mermaid +sequenceDiagram + participant CLI as cli.main + participant RT as runtime + participant ST as SecretStore(_seen) + participant SRV as OpenBao + participant DL as _dispatch_lifecycle + participant UP as cmd_up + CLI->>RT: inject(root, "api") + RT->>RT: store_for(root) → 新規 + RT->>ST: load × 4 + ST->>SRV: login 1 + GET 4(team/global, users/me/global, team/projects/api, users/me/projects/api) + CLI->>DL: dispatch + DL->>RT: clear_injected() + DL->>DL: _resolve_project_name("web") → chdir + DL->>RT: inject(root, "web")(_inject_secrets) + RT->>RT: store_for(root) → 同じ + RT->>ST: load × 4 + ST->>SRV: GET 2(team/projects/web, users/me/projects/web)。共通の 2 参照は _seen + DL->>UP: cmd_up + UP->>RT: store_for(root).exists × 2(_ensure_env_files) + RT->>ST: _seen から返す(GET 0) + UP->>RT: inject(root, "web")(_run_deploy_pipeline) + RT->>ST: _seen から返す(GET 0) + UP-->>DL: 戻る + DL->>RT: release_store()(finally) +``` + +| 経路 | 認証 | GET | 内訳 | +| --- | ---: | ---: | --- | +| `devbase up`(`web` の中) | 1 | 4 | `_load_secret_env` で 4。以降はすべて `_seen` | +| `devbase up web`(`api` の中) | 1 | 6 | `_load_secret_env` で `api` の 4、切替後に `web` の 2 | +| `devbase up web`(`projects/` の外) | 1 | 4 | `_load_secret_env` で共通 2、切替後に `web` の 2 | + +TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起動時に作った `SecretStore` を +最初の操作が引き継ぎ、その操作の `finally` で捨てる。2 回目以降の操作は `_inject_secrets` が +作り直すので、操作ごとに現物を読む。**最初の操作だけは TUI の起動時に読んだ値で起動する** +(決定 3)。 + +### `store_for` の規則 + +| 状況 | 返すもの | +| --- | --- | +| 控えが無い | `SecretStore(root)` を作って控え、返す | +| 控えがあり `root` が同じ | 控えを返す | +| 控えがあり `root` が違う | 捨てて作り直す(テストが `tmp_path` を変えて呼ぶ形に耐える) | +| `release_store()` の後 | 控えが無い状態に戻る | + +`resolve(store=...)` で明示的に渡された `SecretStore` は控えに入れない。移行 +(`env backend migrate`)のように設定と違う backend を相手にする処理が、以後の解決へ +混ざらないためである。 + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| 性能・拡張性 | `up` 1 回の往復が認証 1 回 + 参照ごとに 1 回 | 上の「処理の流れ」。`SecretStore` の寿命をライフサイクル操作 1 回に揃える | `tests/cli/test_up_roundtrips.py` で `FakeOpenBao.logins == 1` と GET の内訳。実機は `devbase --verbose up` のログで認証の行を数える(リリース後テスト) | +| 運用・保守性 | 往復回数を偽サーバのテストで固定し、経路を足したときに増えたことが分かる | 3 経路それぞれのテストが `openbao.requests_of('GET')` の `kv_path` を並べて比べる(件数だけでなく内訳) | テストを読む | + +## 決定の記録 + +### 決定 1: 注入の回数ではなく `SecretStore` の寿命を変える + +3 か所の注入は、それぞれ別の理由で置かれている。 + +| 注入 | 理由 | +| --- | --- | +| `_load_secret_env` | dispatch 前に現在地の機密を載せる(エディタ起動などが従来どおり動く) | +| `_dispatch_lifecycle` | 切替後に切替元の機密を落として載せ直す | +| `_run_deploy_pipeline` | 起動直前に必須として読む(鍵が無ければここで止める) | + +どれか 1 つを消すと、切替の回帰テスト(`tests/cli/test_project_name_resolution.py`)が守って +いる性質を崩す。往復が増えている原因は注入の回数ではない。注入のたびに `SecretStore` を +作り直して `_seen` を捨てていることである。寿命を延ばせば、注入の回数はそのままで往復だけが +減る。 + +`_load_secret_env` の `SecretEnv` を dispatch 先へ引数で渡す案(#168 の案の 1 つ目)は採らない。 +`_dispatch_lifecycle` の handler 群と `cmd_up` の引数が増え、TUI の呼び出し(`tui/dispatch.py`) +も変わる。控えは `SecretStore` が既に持っているので、渡すべきものは無い。 + +### 決定 2: 控えの置き場は `runtime` モジュールに置き、`_dispatch_lifecycle` の `finally` で捨てる + +`docker_context` が同じ形(モジュールの控えと `reset()`)で接続先を持ち回っている。 +`_dispatch_lifecycle` の `finally` には既に `docker_context.reset()` がある。同じ場所に +`release_store()` を並べれば、寿命の規則が 1 か所で読める。 + +`_dispatch_lifecycle` の**入口**で捨てる案は採らない。CLI では `_load_secret_env` が作った +`SecretStore` を捨てることになり、認証が 2 回に戻る。 + +### 決定 3: TUI の最初の操作は起動時に読んだ値で起動する + +出口で捨てる規則の帰結である。TUI の起動から最初の操作までの間にサーバ側の値が変わって +いても、その操作には反映しない。TUI は対話的で、起動から操作までは通常数秒〜数分である。 +起動時の値は同じプロセスが `os.environ` に載せたものと同じで、これまでも `_inject_secrets` が +上書きするまでは子プロセスへ渡っていた。 + +起動からの経過時間で捨てる案は採らない。境界の値を決める根拠が無く、テストで時刻を +偽る手間が増える。 + +### 決定 4: `_ensure_env_files` の存在判定の意味は変えない + +`exists()` の意味(ファイル backend はファイルの有無、`openbao` は取得した内容が空でない)は +そのままにする。持ち回った `SecretStore` に置き換えるだけで、`openbao` では `_seen` から +返るので往復が消える。 + +注入済みの `SecretEnv.global_names` で判定する案(#168 の案の 2 つ目)は採らない。age の +空ファイルは今日「存在する」と判定されるが、`global_names` は空になり `env init` が走る。 +ファイル backend の振る舞いが変わる(PLAN51 前提 3 に触れる)。 + +## テスト設計 + +| 受け入れ条件(PLAN55) | 何で確かめるか | +| --- | --- | +| 1. `web` の中で `up`: 認証 1、GET 4 | `tests/cli/test_up_roundtrips.py::test_up_in_project`。`cli.main(['up'])` 相当を `cwd=projects/web` で走らせ、docker を差し替える。`openbao.logins == 1`、GET の `kv_path` 4 件の集合を比べる | +| 2. `api` の中で `up web`: 認証 1、GET ≤ 6、`api` 固有キーが残らない | 同 `::test_up_other_project`。GET の集合が `api` の 4 + `web` の 2 で、`os.environ` に `api` だけのキーが無い | +| 3. `projects/` の外で `up web`: 認証 1、GET 4 | 同 `::test_up_from_outside` | +| 4. `_ensure_env_files` がサーバへ GET を出さない | 同 `::test_ensure_env_files_reads_seen`。注入の後に `_ensure_env_files()` を呼び、GET が増えない | +| 5. backend `age` で 3 経路の結果が同じ | 既存の `tests/cli/test_project_name_resolution.py` / `tests/commands/test_container_up_order.py` / `tests/commands/test_container_context.py` が変更なしで通る | +| 6. 切替の回帰テストが通る | `tests/cli/test_project_name_resolution.py` を変更しない | +| 7. `pytest` / `ruff` / `compileall` | `quality-gates` | +| `store_for` の規則の表 | `tests/env/test_runtime_store.py`(同一性・`root` 変更・解放) | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| `tests/cli/` の既存 harness が `SecretStore` を直接作っている箇所 | `runtime.resolve(store=...)` で渡している箇所は影響を受けない。`SecretStore(root)` を各テストで作って `monkeypatch` している箇所があれば、`release_store()` を `conftest` の autouse fixture で呼ぶ。実装時に数える | +| PLAN54(#169)との順序 | PLAN54 の `_push_bao_token` は `store_for(root)` から token を取れば、`_run_deploy_pipeline` に `SecretStore` を渡す配線が要らない。PLAN55 を先にマージするのが簡単 | diff --git a/issues/PLAN55_up-single-injection.md b/issues/PLAN55_up-single-injection.md new file mode 100644 index 00000000..2ae01457 --- /dev/null +++ b/issues/PLAN55_up-single-injection.md @@ -0,0 +1,159 @@ +# PLAN55: `devbase up` の機密の注入を 1 回にし、サーバ backend の往復を設計の想定へ収める + +- 発端: #168 +- ワークフローモード: `standard` + - 根拠: `devbase up` の起動経路(`lib/devbase/cli.py` / `lib/devbase/commands/container.py`) + の振る舞いの変更。公開インタフェースは変えない。対象には `tests/cli/test_secret_injection.py` + / `tests/cli/test_project_name_resolution.py` / `tests/commands/test_container_up_order.py` + / `tests/env/test_openbao.py` がある +- 閉じる課題: #168 + +## 依頼(原文) + +> `devbase up` は次の 2 か所で機密を注入する。 +> +> 1. `cli._load_secret_env()` — dispatch の前に現在地のプロジェクトの機密を `runtime.inject()` で載せる +> 2. `commands/container._inject_secrets()` — プロジェクト解決の後に `clear_injected()` → `runtime.inject()` で載せ直す +> +> さらに `_ensure_env_files()` が `SecretStore.exists()` を 2 回呼ぶ。ファイル backend ではファイルの存在確認なので無視できるが、サーバ backend では `SecretStore` インスタンスごとに認証 + 参照ごとの GET が走るため、1 回の `up` で認証 2 回 + GET 10 回程度になる。 +> +> 仕様(`docs/specifications/secret-backend.md` の「OpenBao との契約」)は「`devbase up` 1 回あたり認証 1 回 + 参照ごとに 1 回」を想定しており、`runtime.resolve()` 単位ではその回数に収まっているが(`tests/env/test_openbao.py` で固定)、CLI 全体としては超えている。 +> +> ## 案 +> +> - `_load_secret_env` の注入結果(`SecretEnv` と `SecretStore`)を dispatch 先へ渡し、プロジェクトが変わらないなら載せ直さない +> - `_ensure_env_files` は注入済みの `SecretEnv` から存在を判定する + +## 目的 + +- `devbase up`(プロジェクト内から・`up `・`project up ` の 3 経路)1 回の + サーバへの往復を、仕様の「認証 1 回 + 参照ごとに 1 回(プロジェクト指定ありで 4 回)」に + 収める +- 注入の結果(環境変数へ載る値)と、プロジェクト切替時に切替元の機密が残らない性質を変えない + +## 現状の往復(調査で確定した事実) + +`up ` の経路で `SecretStore` が作られる箇所と、それぞれの往復: + +| 箇所 | 何をするか | 認証 | GET | +| --- | --- | --- | --- | +| `cli._load_secret_env` | 現在地のプロジェクトで `runtime.inject` | 1 | 2 または 4 | +| `container._dispatch_lifecycle`(name 指定時) | `clear_injected` → `_inject_secrets` | 1 | 4 | +| `container._ensure_env_files` | `SecretStore.exists` × 2(同じインスタンス内なので GET は参照ごとに 1 回) | 1 | 2 | +| `container._run_deploy_pipeline` | `_inject_secrets(required=True)` | 1 | 4 | + +合計: 認証 4 回、GET 12〜14 回(`_ensure_env_files` は控えのある参照でも GET する)。 + +## 前提 + +- 前提 1: 同じプロセスの中で、同じ `devbase_root` と同じプロジェクト名に対する解決結果は + 1 回の `runtime.resolve()` で足りる(`up` の途中で他の誰かがサーバ側を書き換えても、 + その `up` は最初に読んだ値で起動する。従来の 2 度注入でも途中で値が変わる保証は無かった)。 + 成否の判定: 受け入れ条件 1 の回数 +- 前提 2: プロジェクトが切り替わったとき(`up ` を別のプロジェクトの中から打つ)は、 + 切替先で改めて解決する。このとき認証はプロセスで 1 回のまま(`SecretStore` を使い回す) + でよい。成否の判定: 受け入れ条件 2 +- 前提 3: ~~`_ensure_env_files` の存在判定は注入済みの結果で置き換える~~ → 存在判定の意味は + 変えず、注入と同じ `SecretStore` を使って往復だけを無くす(2026-09-14、設計の決定 4。 + `SecretEnv` で判定すると age の空ファイルの扱いが変わる)。 + 成否の判定: 受け入れ条件 4 +- 前提 4: ファイル backend(`age` / `plaintext`)の振る舞いと結果は変えない(PLAN51 前提 3)。 + 成否の判定: 受け入れ条件 5 + +## 対象範囲 + +含む: + +- `cli._load_secret_env` → `container` の各経路で `SecretStore` と解決結果を引き継ぐ仕組み +- `_ensure_env_files` の存在判定を注入済みの結果で行うこと +- `up` 1 回の往復回数を偽サーバで固定するテスト +- `docs/specifications/secret-backend.md`「OpenBao との契約」の該当箇所の追記(`plan-to-spec`) + +含まない: + +- `down` / `logs` / `ps` など `required=False` の経路の往復(`up` ほど多くない。数えて + 仕様を超えていれば範囲外として起票する) +- `runtime.resolve()` の重ね順・`SecretStore` の HTTP の契約の変更 +- TUI(1 プロセスで複数の操作を続ける経路)の往復。`_dispatch_lifecycle` の入口で + `docker_context.reset()` と同じ扱いにするかは設計で決めるが、TUI の往復数は条件にしない +- コンテナへの `bao` の導入(#169、PLAN54) + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| 注入 | `runtime.inject()` で `os.environ` に機密を載せること | +| 往復 | OpenBao への HTTP 要求 1 回。認証(`POST …/login`)と取得(`GET`)を分けて数える | +| 3 経路 | `devbase up`(プロジェクト内)/ `devbase up ` / `devbase project up ` | + +## 受け入れ条件 + +- [ ] 前提: backend が `openbao`(偽サーバ)で、プロジェクト `web` の中から実行する + 操作: `devbase up`(docker の呼び出しは差し替える) + 結果: 偽サーバへの認証が 1 回、GET が 4 回(`team/global` / `team/projects/web` / + `users//global` / `users//projects/web` が各 1 回) +- [ ] 前提: プロジェクト `api` の中から実行する + 操作: `devbase up web` + 結果: 認証 1 回。GET は 6 回以下(`api` の 4 参照と `web` の 4 参照のうち、共通の + 2 参照 `team/global` / `users//global` を 2 度取らない。切替先が分かった時点で + 解決するなら 4 回)。起動時の環境変数に `api` 固有のキーが残っていない +- [ ] 前提: `$DEVBASE_ROOT` の外から実行する(現在地にプロジェクトが無い) + 操作: `devbase up web` + 結果: 認証 1 回、GET 4 回 +- [ ] 前提: 注入が済んでいる(`web` の中で `_load_secret_env` 相当を通した後) + 操作: `_ensure_env_files()` を呼ぶ + 結果: 偽サーバへの GET が増えない(判定の結果は変更前と同じ) + ~~前提: `team/projects/web` がキー 0 件 → `env init` を起動しない~~(2026-09-14、 + 設計の決定 4 で存在判定の意味を変えないことにした) +- [ ] backend が `age` のとき、`up` の 3 経路すべてで、環境変数に載る値・生成される + `.docker-compose.scale.yml`・`env init` の起動の有無が変更前と同じ + (既存の `tests/cli/` / `tests/commands/` が変更なしで通る) +- [ ] `tests/cli/test_project_name_resolution.py` の切替の回帰テスト(切替元の機密が残らない)が + 変更なしで通る +- [ ] `uv run pytest tests/` が全件通り、`ruff check lib` と `python -m compileall -q lib bin` + が変更前と同じ結果 + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | `up` 1 回の往復が認証 1 回 + 参照ごとに 1 回(実サーバの実測は 4 参照で 369〜392 ms。往復が半分以下になることを実機で確かめる) | +| 運用・保守性 | 往復回数を偽サーバのテストで固定し、経路を足したときに増えたことが分かる | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わらない | +| データ | 変わらない | +| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run pytest tests/`(偽サーバは `tests/conftest.py` の `FakeOpenBao`) | +| 静的解析 | `ruff check lib`、`python -m compileall -q lib bin` | +| 手動確認 | 利用者の端末(backend `openbao`、PLAN53 の後)で `devbase --verbose up` のログの認証回数を数える。リリース後テストの工程で行う | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | `docs/developer/architecture.md`。起動経路は `lib/devbase/cli.py` と `lib/devbase/commands/container.py`、解決は `lib/devbase/env/runtime.py` | +| コーディング規約 | `docs/developer/contributing.md`。CI は `compileall` / `ruff` / `shellcheck` | +| テスト戦略 | 往復回数は `FakeOpenBao` で固定(`tests/env/test_openbao.py` と同じ道具)。経路ごとの振る舞いは `tests/cli/` / `tests/commands/` の既存の harness に足す。実サーバは手動確認 | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、`ruff`。切替の回帰テストを壊さない | +| 確認してから行う | `runtime.inject` / `clear_injected` の引数の追加(他のモジュールが呼ぶ)。設計 Pull Request のマージ | +| 行わない | HTTP の契約の変更。TUI の往復の最適化。`required=False` の経路の変更 | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| ~~解決結果を引き継ぐ置き場~~ → 決まった: `runtime` モジュールが `SecretStore` を持ち回り、`_dispatch_lifecycle` の `finally` で捨てる(設計の決定 1・2) | 設計 Pull Request のマージで利用者が承認する | 設計 | From 5d94667f0fc058226141b3696416101c5cd9a2cb Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:41:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs(PLAN55):=20=E5=AF=BE=E8=B1=A1=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E3=81=AE=20=5Fensure=5Fenv=5Ffiles=20=E3=81=AE?= =?UTF-8?q?=E8=A8=98=E8=BF=B0=E3=82=92=E5=89=8D=E6=8F=90=203=E3=83=BB?= =?UTF-8?q?=E6=B1=BA=E5=AE=9A=204=20=E3=81=AB=E6=8F=83=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 対象範囲「含む」の「存在判定を注入済みの結果で行う」は、前提 3 と設計の決定 4 で 棄却した旧案の文言だった。注入と同じ SecretStore(runtime.store_for)で 往復だけを無くし、判定の意味は変えない旨に改める(旧文言は取り消し線で残す)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN55_up-single-injection.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/issues/PLAN55_up-single-injection.md b/issues/PLAN55_up-single-injection.md index 2ae01457..dde02971 100644 --- a/issues/PLAN55_up-single-injection.md +++ b/issues/PLAN55_up-single-injection.md @@ -65,7 +65,9 @@ 含む: - `cli._load_secret_env` → `container` の各経路で `SecretStore` と解決結果を引き継ぐ仕組み -- `_ensure_env_files` の存在判定を注入済みの結果で行うこと +- ~~`_ensure_env_files` の存在判定を注入済みの結果で行うこと~~ → `_ensure_env_files` の + 存在判定で注入と同じ `SecretStore`(`runtime.store_for`)を使い、往復を無くすこと + (判定の意味は変えない。2026-09-14、前提 3・設計の決定 4 に揃えた) - `up` 1 回の往復回数を偽サーバで固定するテスト - `docs/specifications/secret-backend.md`「OpenBao との契約」の該当箇所の追記(`plan-to-spec`) From 6a69d28b6abcf04703ffb6515aa24954801f3e6d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:50:33 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs(PLAN55):=20env=20init=20=E3=81=AE?= =?UTF-8?q?=E5=BE=8C=E3=81=AB=E6=8E=A7=E3=81=88=E3=82=92=E6=8D=A8=E3=81=A6?= =?UTF-8?q?=E3=81=A6=E8=AA=AD=E3=81=BF=E7=9B=B4=E3=81=99=E8=A6=8F=E5=89=87?= =?UTF-8?q?=EF=BC=88=E6=B1=BA=E5=AE=9A=205=EF=BC=89=E3=81=A8=E5=BE=80?= =?UTF-8?q?=E5=BE=A9=E6=95=B0=E3=81=AE=E8=A1=A8=E3=81=AE=E7=B2=BE=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 設計: 決定 5 を追加。`_ensure_env_files` が子プロセスの `env init` を走らせたら `runtime.release_store()` で控えを捨て、`_run_deploy_pipeline` が現物を読む。 該当参照だけ `fetch` する案は採らない理由も記す。F4・往復表の 4 行目・ テスト設計 8・`pre-up` の未確認事項を足す - 仕様: 前提 1 の例外と前提 5、受け入れ条件 8、影響の行を揃える - 仕様: 現状分析の `_ensure_env_files` の GET を 1〜2(ローカル .env が無ければ 2)に 直し、合計を 11〜14 に改める Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN55_up-single-injection-design.md | 29 ++++++++++++++++++--- issues/PLAN55_up-single-injection.md | 19 +++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/issues/PLAN55_up-single-injection-design.md b/issues/PLAN55_up-single-injection-design.md index 21059763..557c6ff1 100644 --- a/issues/PLAN55_up-single-injection-design.md +++ b/issues/PLAN55_up-single-injection-design.md @@ -14,6 +14,7 @@ | F1 | `devbase up`(3 経路)が、backend `openbao` でも認証 1 回 + 参照ごとに GET 1 回で起動する | 開発者(意識せずに使う) | | F2 | `_ensure_env_files` の存在判定がサーバへ問い合わせない | 開発者(同上) | | F3 | `up` 1 回の往復回数がテストで固定される | 保守する人 | +| F4 | 共通機密が未作成で `env init` を走らせた `up` は、`env init` が書いた変数でコンテナを起動する(従来どおり) | 開発者(初回の `up`) | ## 構成要素 @@ -22,11 +23,11 @@ | `env/runtime.py` `store_for(root)`(足す) | プロセス内で持ち回る `SecretStore` を返す。無ければ作る。`root` が変われば作り直す | | `env/runtime.py` `release_store()`(足す) | 持ち回っている `SecretStore` を捨てる。次の `store_for` は作り直す | | `env/runtime.py` `resolve()` / `inject()` / `child_env()`(変える) | `store` 引数が `None` のとき `SecretStore(root)` ではなく `store_for(root)` を使う。引数の形は変えない | -| `commands/container.py` `_ensure_env_files()`(変える) | `SecretStore(devbase_root)` を `runtime.store_for(devbase_root)` に置き換える | +| `commands/container.py` `_ensure_env_files()`(変える) | `SecretStore(devbase_root)` を `runtime.store_for(devbase_root)` に置き換える。子プロセスの `env init` を走らせたら、戻った直後に `runtime.release_store()` を呼ぶ(決定 5) | | `commands/container.py` `_dispatch_lifecycle()`(変える) | `finally` で `docker_context.reset()` に並べて `runtime.release_store()` を呼ぶ | | `cli.py` `_load_secret_env()`(変えない) | dispatch 前の注入はそのまま。作った `SecretStore` が `store_for` の控えになる | | `tests/env/test_runtime_store.py`(新設) | `store_for` / `release_store` の振る舞い(同一性・`root` 変更・解放後の作り直し) | -| `tests/cli/test_up_roundtrips.py`(新設) | 3 経路の `up` を `FakeOpenBao` で走らせ、認証と GET の回数を固定する | +| `tests/cli/test_up_roundtrips.py`(新設) | 3 経路の `up` を `FakeOpenBao` で走らせ、認証と GET の回数を固定する。`env init` を走らせた `up` が書いた値で起動することも固定する | | `docs/specifications/secret-backend.md`「OpenBao との契約」(`plan-to-spec` で変える) | 「`devbase up` 1 回あたり」の文を、持ち回りの規則とともに確定仕様にする | 構成要素の関係: @@ -54,6 +55,7 @@ graph TD DL -->|finally| RS INJ --> RES ENS --> SF + ENS -.env init の後.-> RS DEP --> INJ RES --> SF SF --> ST @@ -118,7 +120,7 @@ sequenceDiagram RT->>ST: load × 4 ST->>SRV: GET 2(team/projects/web, users/me/projects/web)。共通の 2 参照は _seen DL->>UP: cmd_up - UP->>RT: store_for(root).exists × 2(_ensure_env_files) + UP->>RT: store_for(root).exists × 1〜2(_ensure_env_files。プロジェクト側はローカル .env が無いときだけ) RT->>ST: _seen から返す(GET 0) UP->>RT: inject(root, "web")(_run_deploy_pipeline) RT->>ST: _seen から返す(GET 0) @@ -131,6 +133,7 @@ sequenceDiagram | `devbase up`(`web` の中) | 1 | 4 | `_load_secret_env` で 4。以降はすべて `_seen` | | `devbase up web`(`api` の中) | 1 | 6 | `_load_secret_env` で `api` の 4、切替後に `web` の 2 | | `devbase up web`(`projects/` の外) | 1 | 4 | `_load_secret_env` で共通 2、切替後に `web` の 2 | +| `devbase up`(`web` の中、`team/global` が未作成) | 2 | 8 | `_load_secret_env` で 4(`team/global` は 404 → 空)。`env init` の後に控えを捨て、`_run_deploy_pipeline` で 4(決定 5) | TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起動時に作った `SecretStore` を 最初の操作が引き継ぎ、その操作の `finally` で捨てる。2 回目以降の操作は `_inject_secrets` が @@ -207,6 +210,24 @@ TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起 空ファイルは今日「存在する」と判定されるが、`global_names` は空になり `env init` が走る。 ファイル backend の振る舞いが変わる(PLAN51 前提 3 に触れる)。 +### 決定 5: 子プロセスの `env init` がストアへ書いたら、控えを捨てて読み直す + +`_ensure_env_files` は共通機密が無いとき、子プロセスで `devbase env init` を走らせる。 +書くのは子プロセスなので、親の `SecretStore` の `_seen` は更新されない。`openbao` では +最初の 404 が `_seen` に空として残り(`OpenBaoBackend.fetch`)、そのまま持ち回ると後続の +`_run_deploy_pipeline` の注入も空の共通機密を使う。今日は `_run_deploy_pipeline` が +`SecretStore` を作り直しているので `env init` が書いた変数は渡っている。寿命を延ばすと +これを失う。 + +規則: `_ensure_env_files` は `env init` の子プロセスから戻ったら、終了コードによらず +`runtime.release_store()` を呼ぶ。次の `store_for` が作り直し、`_run_deploy_pipeline` は +現物を読む。この `up` に限り認証 1 回 + GET 4 回が足される(上の表の 4 行目)。 + +捨てる代わりに `store.fetch(SecretRef.for_global())` で該当参照だけ取り直す案は採らない。 +`env init` が書く参照の一覧を `_ensure_env_files` が知っていなければならず、`env init` の +収集器が書く先を増やしたときに追随を忘れる。初回の `up` だけの 1 往復を惜しんで結合を +増やす理由が無い。ファイル backend は `_seen` を持たず、捨てても変わらない(PLAN51 前提 3)。 + ## テスト設計 | 受け入れ条件(PLAN55) | 何で確かめるか | @@ -218,6 +239,7 @@ TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起 | 5. backend `age` で 3 経路の結果が同じ | 既存の `tests/cli/test_project_name_resolution.py` / `tests/commands/test_container_up_order.py` / `tests/commands/test_container_context.py` が変更なしで通る | | 6. 切替の回帰テストが通る | `tests/cli/test_project_name_resolution.py` を変更しない | | 7. `pytest` / `ruff` / `compileall` | `quality-gates` | +| 8. `team/global` 未作成で `up`: `env init` が書いた値で起動する | 同 `::test_up_after_env_init_reads_written_values`。`FakeOpenBao` の `team/global` を未作成にし、`container.subprocess.run` を「偽サーバへ `INIT_KEY=value` を `save` して 0 で戻る」スタブに差し替える。`_run_deploy_pipeline` へ渡る `SecretEnv` と `os.environ` に `INIT_KEY` があり、`openbao.logins == 2`、GET が 8 件以下 | | `store_for` の規則の表 | `tests/env/test_runtime_store.py`(同一性・`root` 変更・解放) | ## 未確認のまま残ること @@ -225,4 +247,5 @@ TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起 | 項目 | 内容 | | --- | --- | | `tests/cli/` の既存 harness が `SecretStore` を直接作っている箇所 | `runtime.resolve(store=...)` で渡している箇所は影響を受けない。`SecretStore(root)` を各テストで作って `monkeypatch` している箇所があれば、`release_store()` を `conftest` の autouse fixture で呼ぶ。実装時に数える | +| `pre-up` フックが OpenBao へ書く運用があるか | `./pre-up` も子プロセスで、`env init` と同じく親の `_seen` を更新しない。手元にある `projects/*/pre-up` は 1 本(`carmo-system-console`。S3 から平文 `.env` を取る。`bao` / `env set` / `env import` を含まない)で OpenBao へは書かない。書く運用が見つかれば `_run_pre_up_hook` の後にも決定 5 の規則を置く。実装時に `projects/*/pre-up` を読んで数える | | PLAN54(#169)との順序 | PLAN54 の `_push_bao_token` は `store_for(root)` から token を取れば、`_run_deploy_pipeline` に `SecretStore` を渡す配線が要らない。PLAN55 を先にマージするのが簡単 | diff --git a/issues/PLAN55_up-single-injection.md b/issues/PLAN55_up-single-injection.md index dde02971..3a3b00fe 100644 --- a/issues/PLAN55_up-single-injection.md +++ b/issues/PLAN55_up-single-injection.md @@ -39,16 +39,19 @@ | --- | --- | --- | --- | | `cli._load_secret_env` | 現在地のプロジェクトで `runtime.inject` | 1 | 2 または 4 | | `container._dispatch_lifecycle`(name 指定時) | `clear_injected` → `_inject_secrets` | 1 | 4 | -| `container._ensure_env_files` | `SecretStore.exists` × 2(同じインスタンス内なので GET は参照ごとに 1 回) | 1 | 2 | +| `container._ensure_env_files` | `SecretStore.exists` × 1〜2(共通は常に、プロジェクトはローカル `.env` が無いときだけ。同じインスタンス内なので GET は参照ごとに 1 回) | 1 | 1〜2(ローカル `.env` が無ければ 2) | | `container._run_deploy_pipeline` | `_inject_secrets(required=True)` | 1 | 4 | -合計: 認証 4 回、GET 12〜14 回(`_ensure_env_files` は控えのある参照でも GET する)。 +合計: 認証 4 回、GET ~~12〜14~~ → 11〜14 回(2026-09-14、`_ensure_env_files` のプロジェクト側の +`exists` はローカル `.env` が無いときだけ呼ばれる。控えのある参照でも GET する点は変わらない)。 ## 前提 - 前提 1: 同じプロセスの中で、同じ `devbase_root` と同じプロジェクト名に対する解決結果は 1 回の `runtime.resolve()` で足りる(`up` の途中で他の誰かがサーバ側を書き換えても、 - その `up` は最初に読んだ値で起動する。従来の 2 度注入でも途中で値が変わる保証は無かった)。 + その `up` は最初に読んだ値で起動する。従来の 2 度注入でも途中で値が変わる保証は無かった。 + ~~例外なし~~ → ただし、その `up` 自身が `env init` で書いた分は読み直す。2026-09-14、 + 前提 5)。 成否の判定: 受け入れ条件 1 の回数 - 前提 2: プロジェクトが切り替わったとき(`up ` を別のプロジェクトの中から打つ)は、 切替先で改めて解決する。このとき認証はプロセスで 1 回のまま(`SecretStore` を使い回す) @@ -59,6 +62,9 @@ 成否の判定: 受け入れ条件 4 - 前提 4: ファイル backend(`age` / `plaintext`)の振る舞いと結果は変えない(PLAN51 前提 3)。 成否の判定: 受け入れ条件 5 +- 前提 5: 共通機密が未作成で `_ensure_env_files` が子プロセスの `env init` を走らせたとき、 + `env init` が書いた変数はその `up` のコンテナへ渡る(今日はそうなっている。控えを持ち回る + ことでこれを失わない。2026-09-14、設計の決定 5)。成否の判定: 受け入れ条件 8 ## 対象範囲 @@ -114,6 +120,11 @@ 変更なしで通る - [ ] `uv run pytest tests/` が全件通り、`ruff check lib` と `python -m compileall -q lib bin` が変更前と同じ結果 +- [ ] 前提: backend が `openbao`(偽サーバ)で `team/global` が未作成。`env init` の子プロセスは + 偽サーバへ `INIT_KEY=value` を保存して成功終了するものに差し替える + 操作: `web` の中で `devbase up`(docker の呼び出しは差し替える) + 結果: 起動時の環境変数と生成される構成に `INIT_KEY` が渡る。往復は認証 2 回、GET 8 回 + 以下(`env init` の前に 4、書いた後に読み直して 4)(2026-09-14、前提 5) ## 非機能の条件 @@ -128,7 +139,7 @@ | --- | --- | | 公開インタフェース | 変わらない | | データ | 変わらない | -| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる | +| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる。共通機密が未作成で `env init` を走らせたときだけ、書いた後に読み直す(その `up` に限り認証 1 回 + GET 4 回が足される) | ## 検証手段 From e9547f30c0ac7e96903c1ddc10f94362a18ae1d6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 17:21:07 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(PLAN55):=20TUI=20=E3=81=AF=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E3=81=AE=E5=85=A5=E5=8F=A3=E3=81=A7=E6=8E=A7=E3=81=88?= =?UTF-8?q?=E3=82=92=E6=8D=A8=E3=81=A6=E3=82=8B=EF=BC=88=E6=B1=BA=E5=AE=9A?= =?UTF-8?q?=203=20=E3=82=92=E6=94=B9=E3=82=81=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TUI の env edit / sync / init / project は dispatch_group 経由の別の SecretStore で 書くため、起動時の控えを最初の操作へ引き継ぐ旧決定 3 では edit → up が編集前の 値で起動する(codex round 3)。tui/dispatch.py の _preserve_cwd_env の入口で runtime.release_store() を呼ぶ規則に改め、F5・構成要素・往復の表・テスト設計 9 を 足した。仕様側は前提 6・受け入れ条件 9・対象範囲・影響を揃えた。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN55_up-single-injection-design.md | 48 ++++++++++++++++----- issues/PLAN55_up-single-injection.md | 16 ++++++- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/issues/PLAN55_up-single-injection-design.md b/issues/PLAN55_up-single-injection-design.md index 557c6ff1..d67686d7 100644 --- a/issues/PLAN55_up-single-injection-design.md +++ b/issues/PLAN55_up-single-injection-design.md @@ -15,6 +15,7 @@ | F2 | `_ensure_env_files` の存在判定がサーバへ問い合わせない | 開発者(同上) | | F3 | `up` 1 回の往復回数がテストで固定される | 保守する人 | | F4 | 共通機密が未作成で `env init` を走らせた `up` は、`env init` が書いた変数でコンテナを起動する(従来どおり) | 開発者(初回の `up`) | +| F5 | TUI で機密を書いて(`env edit` など)から `up` すると、書いた値でコンテナを起動する(従来どおり) | 開発者(TUI) | ## 構成要素 @@ -26,8 +27,10 @@ | `commands/container.py` `_ensure_env_files()`(変える) | `SecretStore(devbase_root)` を `runtime.store_for(devbase_root)` に置き換える。子プロセスの `env init` を走らせたら、戻った直後に `runtime.release_store()` を呼ぶ(決定 5) | | `commands/container.py` `_dispatch_lifecycle()`(変える) | `finally` で `docker_context.reset()` に並べて `runtime.release_store()` を呼ぶ | | `cli.py` `_load_secret_env()`(変えない) | dispatch 前の注入はそのまま。作った `SecretStore` が `store_for` の控えになる | +| `tui/dispatch.py` `_preserve_cwd_env()`(変える) | TUI の委譲の入口(`dispatch_lifecycle` / `dispatch_group` の両方が通る)で `runtime.release_store()` を呼ぶ。起動時や前の操作の控えを持ち越さない(決定 3) | | `tests/env/test_runtime_store.py`(新設) | `store_for` / `release_store` の振る舞い(同一性・`root` 変更・解放後の作り直し) | | `tests/cli/test_up_roundtrips.py`(新設) | 3 経路の `up` を `FakeOpenBao` で走らせ、認証と GET の回数を固定する。`env init` を走らせた `up` が書いた値で起動することも固定する | +| `tests/cli/tui/test_dispatch.py`(変える) | TUI の委譲の入口で控えが捨てられること。同じプロセスで `env edit` → `up` した値が渡ること | | `docs/specifications/secret-backend.md`「OpenBao との契約」(`plan-to-spec` で変える) | 「`devbase up` 1 回あたり」の文を、持ち回りの規則とともに確定仕様にする | 構成要素の関係: @@ -37,6 +40,9 @@ graph TD subgraph cli [cli.py] LOAD[_load_secret_env] end + subgraph tui [tui/dispatch.py] + TD[_preserve_cwd_env] + end subgraph container [commands/container.py] DL[_dispatch_lifecycle] INJ[_inject_secrets] @@ -51,6 +57,8 @@ graph TD ST[(SecretStore
OpenBaoBackend._seen)] SRV[(OpenBao)] LOAD --> RES + TD -.入口で捨てる.-> RS + TD --> DL DL --> INJ DL -->|finally| RS INJ --> RES @@ -134,11 +142,12 @@ sequenceDiagram | `devbase up web`(`api` の中) | 1 | 6 | `_load_secret_env` で `api` の 4、切替後に `web` の 2 | | `devbase up web`(`projects/` の外) | 1 | 4 | `_load_secret_env` で共通 2、切替後に `web` の 2 | | `devbase up`(`web` の中、`team/global` が未作成) | 2 | 8 | `_load_secret_env` で 4(`team/global` は 404 → 空)。`env init` の後に控えを捨て、`_run_deploy_pipeline` で 4(決定 5) | +| TUI の `up web`(操作 1 回あたり) | 1 | 4 | 入口で捨て、`_inject_secrets` で `web` の 4。起動時の `_load_secret_env` の分(認証 1 + GET 2〜4)は操作に含めない。TUI の往復数は条件にしない(PLAN55 対象範囲) | -TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起動時に作った `SecretStore` を -最初の操作が引き継ぎ、その操作の `finally` で捨てる。2 回目以降の操作は `_inject_secrets` が -作り直すので、操作ごとに現物を読む。**最初の操作だけは TUI の起動時に読んだ値で起動する** -(決定 3)。 +TUI(1 プロセスで操作を続ける)では、委譲の入口(`tui/dispatch.py` の `_preserve_cwd_env`)で +控えを捨てる。`_load_secret_env` が起動時に作った `SecretStore` は最初の操作にも引き継がず、 +毎回 `_inject_secrets` が作り直して現物を読む。**TUI の中で機密を書いてから `up` しても、書いた +値で起動する**(決定 3)。 ### `store_for` の規則 @@ -190,14 +199,31 @@ TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起 `_dispatch_lifecycle` の**入口**で捨てる案は採らない。CLI では `_load_secret_env` が作った `SecretStore` を捨てることになり、認証が 2 回に戻る。 -### 決定 3: TUI の最初の操作は起動時に読んだ値で起動する +### 決定 3: TUI は操作の入口で控えを捨て、操作ごとに現物を読む + +~~決定 3: TUI の最初の操作は起動時に読んだ値で起動する~~(2026-09-14、round 3 で改めた)。 + +旧案は「出口で捨てる」規則の帰結として、起動時の `SecretStore` を最初の操作が引き継ぐものと +していた。しかし TUI の `env` 操作(`edit` / `sync` / `init` / `project`)は `dispatch_group` → +`commands/env.py` の `_secret_store()` が作る**別の** `SecretStore` で書き、`_dispatch_lifecycle` を +通らない。起動 → `env edit` で共通機密を保存 → 最初の `up` の順に操作すると、起動時の `_seen` +が残ったまま `_inject_secrets` / `_run_deploy_pipeline` が編集前の値を読む。今日は +`_run_deploy_pipeline` が `SecretStore` を作り直しているので編集後の値で起動しており、旧案は +これを失う(codex round 3)。 + +規則: TUI の委譲層 `tui/dispatch.py` の `_preserve_cwd_env`(`dispatch_lifecycle` と +`dispatch_group` の両方が通る)の**入口**で `runtime.release_store()` を呼ぶ。CWD と `os.environ` +を操作の前後で復元する境界と同じ場所で、「TUI の操作は起動時や前の操作の状態を引き継がない」 +規則が 1 か所で読める。書く操作を数えて捨てる案(`env edit` の後だけ捨てる)は採らない。 +書く経路(`env set` / `import`、将来の操作)を列挙して追随する結合が増え、決定 5 で退けたのと +同じ理由になる。読むだけの操作の後も捨てるが、失うのは次の操作の認証 1 回 + GET 4 回で、TUI の +往復数は条件にしていない(PLAN55 対象範囲)。 -出口で捨てる規則の帰結である。TUI の起動から最初の操作までの間にサーバ側の値が変わって -いても、その操作には反映しない。TUI は対話的で、起動から操作までは通常数秒〜数分である。 -起動時の値は同じプロセスが `os.environ` に載せたものと同じで、これまでも `_inject_secrets` が -上書きするまでは子プロセスへ渡っていた。 +決定 2 の「入口で捨てない」は `commands/container.py` の `_dispatch_lifecycle`(CLI と TUI の +共有)についての判断で、`_load_secret_env` の控えを CLI が使えるようにするためである。 +`tui/dispatch.py` は TUI だけが通るので、そこで捨てても CLI の往復は変わらない。 -起動からの経過時間で捨てる案は採らない。境界の値を決める根拠が無く、テストで時刻を +起動からの経過時間で捨てる案は引き続き採らない。境界の値を決める根拠が無く、テストで時刻を 偽る手間が増える。 ### 決定 4: `_ensure_env_files` の存在判定の意味は変えない @@ -240,6 +266,8 @@ TUI(1 プロセスで操作を続ける)では、`_load_secret_env` が起 | 6. 切替の回帰テストが通る | `tests/cli/test_project_name_resolution.py` を変更しない | | 7. `pytest` / `ruff` / `compileall` | `quality-gates` | | 8. `team/global` 未作成で `up`: `env init` が書いた値で起動する | 同 `::test_up_after_env_init_reads_written_values`。`FakeOpenBao` の `team/global` を未作成にし、`container.subprocess.run` を「偽サーバへ `INIT_KEY=value` を `save` して 0 で戻る」スタブに差し替える。`_run_deploy_pipeline` へ渡る `SecretEnv` と `os.environ` に `INIT_KEY` があり、`openbao.logins == 2`、GET が 8 件以下 | +| 9. TUI で `env edit` → `up`: 書いた値で起動する | `tests/cli/tui/test_dispatch.py::test_lifecycle_after_env_edit_reads_written_values`。`FakeOpenBao` の `team/global` に `REVIEW_KEY=old` を置き、`_load_secret_env` 相当を通してから、同じプロセスで `dispatch_group(cmd_env, root, 'edit')` をエディタのスタブ(`REVIEW_KEY=new` を保存)で走らせ、`dispatch_lifecycle('up', name='web')` を docker 差し替えで走らせる。`_run_deploy_pipeline` へ渡る `SecretEnv` と子プロセスの環境の `REVIEW_KEY` が `new` | +| 決定 3 の規則 | 同 `::test_preserve_cwd_env_releases_store_on_entry`。`store_for(root)` で控えを作ってから `dispatch_group` を no-op の handler で走らせ、handler の中で `store_for(root)` が別のインスタンスを返す | | `store_for` の規則の表 | `tests/env/test_runtime_store.py`(同一性・`root` 変更・解放) | ## 未確認のまま残ること diff --git a/issues/PLAN55_up-single-injection.md b/issues/PLAN55_up-single-injection.md index 3a3b00fe..5eeb5eab 100644 --- a/issues/PLAN55_up-single-injection.md +++ b/issues/PLAN55_up-single-injection.md @@ -5,7 +5,7 @@ - 根拠: `devbase up` の起動経路(`lib/devbase/cli.py` / `lib/devbase/commands/container.py`) の振る舞いの変更。公開インタフェースは変えない。対象には `tests/cli/test_secret_injection.py` / `tests/cli/test_project_name_resolution.py` / `tests/commands/test_container_up_order.py` - / `tests/env/test_openbao.py` がある + / `tests/env/test_openbao.py` / `tests/cli/tui/test_dispatch.py` がある - 閉じる課題: #168 ## 依頼(原文) @@ -65,6 +65,10 @@ - 前提 5: 共通機密が未作成で `_ensure_env_files` が子プロセスの `env init` を走らせたとき、 `env init` が書いた変数はその `up` のコンテナへ渡る(今日はそうなっている。控えを持ち回る ことでこれを失わない。2026-09-14、設計の決定 5)。成否の判定: 受け入れ条件 8 +- 前提 6: TUI(1 プロセスで操作を続ける)で機密を書いて(`env edit` など)から `up` したとき、 + 書いた値がそのコンテナへ渡る(今日はそうなっている。TUI の書き込みは注入と別の `SecretStore` + を通るので、控えを持ち回ることでこれを失わない。2026-09-14、設計の決定 3)。 + 成否の判定: 受け入れ条件 9 ## 対象範囲 @@ -75,6 +79,8 @@ 存在判定で注入と同じ `SecretStore`(`runtime.store_for`)を使い、往復を無くすこと (判定の意味は変えない。2026-09-14、前提 3・設計の決定 4 に揃えた) - `up` 1 回の往復回数を偽サーバで固定するテスト +- TUI で機密を書いてから `up` したとき、書いた値で起動する性質を保つこと(2026-09-14、 + 前提 6。TUI の往復数は引き続き条件にしない) - `docs/specifications/secret-backend.md`「OpenBao との契約」の該当箇所の追記(`plan-to-spec`) 含まない: @@ -84,6 +90,7 @@ - `runtime.resolve()` の重ね順・`SecretStore` の HTTP の契約の変更 - TUI(1 プロセスで複数の操作を続ける経路)の往復。`_dispatch_lifecycle` の入口で `docker_context.reset()` と同じ扱いにするかは設計で決めるが、TUI の往復数は条件にしない + (決まった: TUI の委譲層 `tui/dispatch.py` の入口で捨てる。2026-09-14、設計の決定 3) - コンテナへの `bao` の導入(#169、PLAN54) ## 用語 @@ -125,6 +132,11 @@ 操作: `web` の中で `devbase up`(docker の呼び出しは差し替える) 結果: 起動時の環境変数と生成される構成に `INIT_KEY` が渡る。往復は認証 2 回、GET 8 回 以下(`env init` の前に 4、書いた後に読み直して 4)(2026-09-14、前提 5) +- [ ] 前提: backend が `openbao`(偽サーバ)で `team/global` に `REVIEW_KEY=old` がある。TUI の + 起動相当(`_load_secret_env`)を通した後、同じプロセスで TUI の `env edit`(エディタは + `REVIEW_KEY=new` を保存するものに差し替える)を実行する + 操作: 同じプロセスで TUI の `up web`(docker の呼び出しは差し替える) + 結果: 起動時の環境変数と生成される構成の `REVIEW_KEY` が `new`(2026-09-14、前提 6) ## 非機能の条件 @@ -139,7 +151,7 @@ | --- | --- | | 公開インタフェース | 変わらない | | データ | 変わらない | -| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる。共通機密が未作成で `env init` を走らせたときだけ、書いた後に読み直す(その `up` に限り認証 1 回 + GET 4 回が足される) | +| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる。共通機密が未作成で `env init` を走らせたときだけ、書いた後に読み直す(その `up` に限り認証 1 回 + GET 4 回が足される)。TUI は操作の入口で控えを捨て、起動時に読んだ値を最初の操作にも引き継がない(TUI の操作 1 回あたり認証 1 回 + GET 4 回。今日より少ない) | ## 検証手段