From fc111e8cb8346a9fdce315129dc1580049f1de92 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 15 Sep 2026 16:04:40 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20PLAN54=20/=20PLAN55=20=E3=82=92?= =?UTF-8?q?=E6=A9=9F=E5=AF=86=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AE=E7=A2=BA?= =?UTF-8?q?=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=B8=E5=8F=96=E3=82=8A=E8=BE=BC?= =?UTF-8?q?=E3=82=80=20(#168,=20#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SecretStore の持ち回り、コンテナの中の bao、devbase env token を docs/specifications/secret-backend.md へ as-is 仕様として書き、計画ファイルを削除する。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WWoEdi3vSQQnLLas1fVNLL --- docs/specifications/secret-backend.md | 148 +++++++- issues/PLAN54_bao-in-container-design.md | 364 -------------------- issues/PLAN54_bao-in-container.md | 192 ----------- issues/PLAN55_up-single-injection-design.md | 279 --------------- issues/PLAN55_up-single-injection.md | 254 -------------- 5 files changed, 141 insertions(+), 1096 deletions(-) delete mode 100644 issues/PLAN54_bao-in-container-design.md delete mode 100644 issues/PLAN54_bao-in-container.md delete mode 100644 issues/PLAN55_up-single-injection-design.md delete mode 100644 issues/PLAN55_up-single-injection.md diff --git a/docs/specifications/secret-backend.md b/docs/specifications/secret-backend.md index 6a8f7049..d17933fc 100644 --- a/docs/specifications/secret-backend.md +++ b/docs/specifications/secret-backend.md @@ -14,6 +14,11 @@ KV v2 シークレットエンジンに対応し、REST を標準ライブラリ あり、`devbase env list` / `get` / `set` / `delete` / `edit` の `--user` で個人単位の置き場を 相手にする。ファイル backend は個人単位の置き場を持たない。 +backend が `openbao` の端末では、dev コンテナの中の OpenBao CLI(`bao`、base イメージに同梱)が +接続先 `BAO_ADDR` と `~/.vault-token` を受け取り、再起動せずに自分の機密を読み書きできる。 +コンテナに置く資格情報は 1 時間で切れる token だけで、切れたらホストの `devbase env token` で +置き換える。 + ### サーバ backend に OpenBao を採る理由 最初のサーバ backend は Infisical を予定していた。Community 版(ライセンス無し)では @@ -43,6 +48,7 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | ブートストラップ機密 | サーバ backend が接続に使う AppRole の `role_id` / `secret_id`。`secrets/bootstrap.env.age` に age で暗号化して置く | | キャッシュ | サーバの内容と一致すると確かめられた機密を、参照ごとに age で暗号化して手元に控えたもの | | scope | キャッシュの取得元を表す指紋。接続先 URL・`mount`・パス・`role_id` の SHA-256 | +| 持ち回る `SecretStore` | 1 回のライフサイクル操作(`up` など)の間、注入と存在判定と token の発行が共有する 1 つのインスタンス。`runtime.store_for()` が返し、`runtime.release_store()` が捨てる | | `role_id` / `secret_id` / token | `role_id` は利用者ごとの AppRole の識別子。`secret_id` は端末ごとに発行される長期の資格情報で手元に保存する。token は両者を交換して得る短期の資格情報でプロセス内にだけ持つ | ## 構成要素 @@ -56,9 +62,12 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | OpenBao adapter | `lib/devbase/env/openbao.py` | AppRole 認証、参照ごとの取得、版を指定した丸ごとの書き込み、失敗の種類の判定 | | ブートストラップ | `lib/devbase/env/bootstrap.py` | 接続資格情報を登録簿を経由せず age で直接読み書きする | | キャッシュ | `lib/devbase/env/cache.py` | 参照ごとの控えの書き込み・読み出し・破棄・全消去 | -| 機密の合成 | `lib/devbase/env/runtime.py` | 4 層の機密を重ねてコンテナへ渡す | +| 機密の合成 | `lib/devbase/env/runtime.py` | 4 層の機密を重ねてコンテナへ渡す。`SecretStore` をライフサイクル操作 1 回の間持ち回る(`store_for` / `release_store`) | +| コンテナへの token の配送 | `lib/devbase/env/container_token.py` | 受け取った token を `docker exec` の stdin で各コンテナの `~/.vault-token` へ書く。token の取得と届け先の解決は持たない | +| `up` / `scale` の後処理 | `lib/devbase/commands/container.py` | backend が `openbao` のとき dev サービスへ `BAO_ADDR` を足し、起動後に token を書く | +| base イメージ | `containers/base/Dockerfile` | OpenBao CLI `bao` を `checksums.txt` で検証して `/usr/local/bin` へ置く | | `env backend` コマンド | `lib/devbase/commands/env_backend.py` | `status` / `use` / `test` / `migrate` | -| `env` コマンド | `lib/devbase/commands/env.py` | `--user` の受け取り、`edit` の分岐、一覧の保存形式表示 | +| `env` コマンド | `lib/devbase/commands/env.py` | `--user` の受け取り、`edit` の分岐、一覧の保存形式表示、`env token` | | `rekey` / `doctor` | `lib/devbase/commands/env_ops.py` | 手元の age 暗号文すべての再暗号化、backend 設定と権限の点検 | | `encrypt` / `decrypt` | `lib/devbase/commands/env_migrate.py` | age ストアと平文の間の移動(backend の向きと突き合わせる) | | `import` | `lib/devbase/env/io_import.py` | サーバ backend の参照への取り込みと age 暗号化した退避。計画の元にした値(`Plan.before`)を退避と巻き戻しに使う | @@ -75,6 +84,10 @@ flowchart LR OB --> CA[cache] BS --> AGEFILE[(secrets/bootstrap.env.age)] CA --> CAFILE[(secrets/cache/)] + CLI --> CT[container_token] + CT -->|docker exec: ~/.vault-token| DEV[dev コンテナの bao] + DEV -->|BAO_ADDR + token| SRV[(OpenBao)] + OB --> SRV ``` ## 仕様 @@ -176,10 +189,11 @@ flowchart LR - 参照ごとに 1 回 `GET` を呼ぶ。`LIST` は使わない(個人単位のパスは利用者ごとに分かれて おり、親から辿ると他人のパスまで要求する。サーバのポリシーも `users/` 直下の一覧を拒む)。 `runtime.resolve()` 1 回はプロジェクト指定ありで認証 1 回 + 取得 4 回、指定なしで認証 - 1 回 + 取得 2 回 + 1 回 + 取得 2 回。`devbase up` 1 回も同じ回数に収まる(「`SecretStore` の持ち回り」) - 同じ `SecretStore` の中では、取得した参照の内容と版を控えて `exists` → `load` の並びで 2 度取りに行かない。この控えの版が、その参照を次に書くときの基準になる -- token は実行のたびに取り直し、ディスクへ保存しない。`lease_duration` の少し前に取り直す +- token は実行のたびに取り直し、ホストのディスクへ保存しない(コンテナの `~/.vault-token` へ + 渡すものは「コンテナの中の `bao`」を参照)。`lease_duration` の少し前に取り直す (エディタを長く開いた `env edit` の書き戻しで、期限切れの token を送らないため)。 応答の状態による再認証は置かない。認証以外の経路の 403 は権限の不足として確定する (サーバ側で token の期限を実行時間より十分長くする前提で、期限切れは事前の取り直しだけで @@ -233,6 +247,98 @@ flowchart LR 運用側のリポジトリ(carmo-cdk#312)が持つ。devbase が前提にするのは、上の 4 経路と 「本人のパスは読み書きでき、チームのパスは読め、他人のパスは拒まれる」ことだけである。 +### `SecretStore` の持ち回り + +`devbase up` は機密を 3 か所で注入する。それぞれ別の理由で置かれている。 + +| 注入 | 理由 | +| --- | --- | +| `cli._load_secret_env` | dispatch の前に現在地の機密を載せる(エディタ起動などが値を使う) | +| `_dispatch_lifecycle`(名前を指定したとき) | 切替元の機密を落として切替先で載せ直す | +| `_run_deploy_pipeline` | 起動の直前に必須として読む(鍵が無ければここで止める) | + +注入の回数は変えず、`SecretStore` の寿命をライフサイクル操作 1 回に揃える。同じインスタンスなら +2 度目以降の解決は控え(取得した内容と版)から返り、サーバへは行かない。 + +- `runtime.resolve` / `inject` / `child_env` は `store` を渡されなければ `store_for(root)` を使う。 + 控えが無ければ作り、`root` が違えば作り直す。明示的に渡された `store`(`migrate` のように + 設定と違う backend を相手にする処理)は控えに入れない +- `_ensure_env_files` の存在判定も `store_for(root)` を使う。判定の意味(ファイル backend は + ファイルの有無、`openbao` は取得した内容が空でない)は変えない +- 捨てる契機は 3 つである + + | 契機 | 理由 | + | --- | --- | + | `_dispatch_lifecycle` の `finally` | 寿命をライフサイクル操作 1 回にする。入口で捨てると、CLI で dispatch 前の注入が作ったものを捨てて認証が 2 回に戻る | + | TUI の委譲の入口(`tui/dispatch.py` の `_preserve_cwd_env`) | TUI は 1 プロセスで操作を続ける。起動時や前の操作の控えを持ち越すと、`env edit` で書いた直後の `up` が編集前の値で起動する | + | `_ensure_env_files` が子プロセスの `env init` から戻った直後(終了コードによらない) | 書いたのは子プロセスで、親の控えには最初の 404 が空として残る。捨てて読み直し、`env init` が書いた値でその `up` を起動する | + +`up` 1 回の往復(サーバ backend、4 参照): + +| 経路 | 認証 | 取得 | +| --- | ---: | ---: | +| `devbase up`(プロジェクト `web` の中) | 1 | 4 | +| `devbase up web`(別のプロジェクト `api` の中) | 1 | 6(`api` の 4 + 切替後の `web` 固有の 2) | +| `devbase up web`(`projects/` の外) | 1 | 4 | +| 共通機密が未作成で `env init` を走らせた `up` | 2 | 8 以下 | + +### コンテナの中の `bao` + +base イメージは OpenBao CLI `bao`(`ARG BAO_VERSION`、サーバと同じ 2.6 系)を含む。amd64 / +arm64 の tar.gz を同じリリースの `checksums.txt` と突き合わせ、対象の行が無い・値が合わない +ときはビルドを止める。`.deb` は systemd ユニットやシステムユーザーを伴うため使わない。 +backend によらず全端末のイメージに入れる(管理スクリプトが `bao` に依存し、管理者の端末の +backend 設定とは関係しない)。 + +backend が `openbao` のとき、`up` と `scale` は dev コンテナへ次を渡す。 + +| 名前 | 形 | いつ | +| --- | --- | --- | +| `BAO_ADDR` | dev サービスの `environment` にリテラル(`openbao.url`)。機密ではない | 構成の生成時 | +| `~/.vault-token` | ファイル `0600`、token 1 行(改行なし) | `up` の [5/6] の後(`scale` は増やしたインスタンスだけ)と `env token` | + +- コンテナに置く資格情報は 1 時間で切れる token だけで、`secret_id` はホストから出ない。 + `role_id` / `secret_id` をコンテナへ渡す形は、コンテナの中の CLI や npm パッケージが長期の + 資格情報を持つことになるため採らない +- token を環境変数(`BAO_TOKEN`)にしない。`docker inspect` と子プロセスの環境に残るため。 + `bao` は `BAO_TOKEN` が無ければ `~/.vault-token` を読む +- token は注入と同じ `SecretStore` の `OpenBaoBackend.issue_token()` から取る(期限内なら + ログインし直さない)。書き込みは `docker exec -i sh -c '…'` の stdin で渡し、 + argv に載せない。コンテナの中では `mktemp "$HOME/.vault-token.XXXXXX"` に書き、 + `chmod 0600` の後 `mv -f` で置き換える(固定名の一時ファイルは既存の inode へ書いて + `umask` が効かない。途中で切れても空の `~/.vault-token` を残さない) +- 書けなくても `up` / `scale` は失敗にしない(起動は済んでおり、`env token` でやり直せる)。 + 警告を出す +- コンテナの `bao` で書いた値は、ホストの控え(`secrets/cache/`)へ反映しない。控えは + 読み取りにだけ使い、ホストの `set` / `delete` / `edit` は現物を読むため、到達できる限り + 食い違わない +- 接続先(`DOCKER_CONTEXT`)は呼び出し側が process の環境へ当てる。`up` / `scale` は + `_resolve_docker_target` が当てた同じ process の中で書くため、別ホストの Docker([remote-docker-context.md](remote-docker-context.md))にも + 同じ形で届く + +### `devbase env token` + +```text +devbase env token [--print] [--context NAME] +``` + +起動中の dev コンテナの `~/.vault-token` を新しい token で置き換える。機密の注入を行わない +コマンドとして扱う(値は要らず、注入するとサーバへの往復が増える)。 + +処理は次の順で行い、上の行で止まれば下は見ない。ログインを届け先が見つかった後に置くのは、 +使われない token をサーバに発行させないためである。 + +| 順 | 処理 | 止まるとき(終了コード 1) | +| --- | --- | --- | +| 1 | backend の判定 | backend が `openbao` でない | +| 2 | `--print` なら、ログインして token を 1 行出して 0 で終わる(プロジェクトもコンテナも見ない) | ログインが拒まれた・到達できない | +| 3 | 実行時のディレクトリからプロジェクトを決める(`-p` と同じく名前は取らない) | `projects/` の下ではない | +| 4 | プロジェクト直下の `env` を載せてから dev サービス名(`DEV_SERVICE_NAME`、既定 `dev`)を取る。下位ディレクトリから打っても同じ名前になる | — | +| 5 | プロジェクト直下の `project.local.yml` と `--context` / `DEVBASE_DOCKER_CONTEXT` から接続先を決めて当てる(`env exec` と同じ優先順) | — | +| 6 | `docker ps --filter label=com.docker.compose.project=` で、サービス名が `-`(`n` は 1 以上)のコンテナを番号順に集める。DB や snapshot は入らない | `docker ps` が失敗した、起動中の dev コンテナが無い | +| 7 | ログイン(`issue_token()`)。控えは使わない | ログインが拒まれた・到達できない | +| 8 | 各コンテナへ書き、書いたコンテナ名を 1 行ずつ出す(token は出さない) | 1 つでも書けなかった | + ### 失敗の種類とキャッシュ サーバとのやり取りの失敗は例外の型で区別し、キャッシュの扱いを決める。 @@ -360,7 +466,10 @@ flowchart TD ### 常に成り立つ条件 - 機密の値と `secret_id` は、ログ・例外メッセージ・`status` の出力・`--dry-run` の出力・ - `index.json` に載らない + `index.json` に載らない。token はログ・構成ファイル・`docker exec` の argv・コンテナの + 環境変数に載らない(`env token --print` の標準出力だけに出る) +- `secret_id` はホストの `bootstrap.env.age` から出ない。コンテナに置かれる資格情報は + `~/.vault-token`(`0600`)の token だけである - `backend.yml` に機密は入らない。ブートストラップとキャッシュは age 暗号文としてしか ディスクに置かれない。age の識別鍵が無い端末では平文へ落とさず、鍵の用意を促して非ゼロで 終了する @@ -448,6 +557,10 @@ cache: - 端末ごとに `secret_id` を分けるため、1 台の失効が他の端末に及ばない。失効前に発行済みの token は期限まで使えるため、期限はサーバ側で短く保つ - `secrets/` は Git の除外対象で、`doctor` が実際に除外されることを確かめる +- コンテナの `~/.vault-token` を読める者は、その token の期限(1 時間)まで本人として読み書き + できる。長期の資格情報はコンテナへ置かない +- base イメージの `bao` は同じリリースの `checksums.txt` で検証する(署名は検証しない。 + 他のツールと同じ扱い) ## 運用 @@ -460,6 +573,9 @@ cache: - 1 台の端末が同時に使う backend は 1 つ、扱う個人単位の機密は 1 人分である - 複数人の同時編集は版の不一致として後から書いた側で止まる。黙って上書きせず、読み直して やり直す。不達のときに書き込みを控えへ溜める経路は無い +- コンテナの `bao` の token は 1 時間で切れる。`permission denied`(403)が出たらホストの + プロジェクトのディレクトリで `devbase env token` を打つ。`bao` の版を上げるときは + `containers/base/Dockerfile` の `ARG BAO_VERSION` 1 行を変えて base イメージを作り直す - 個人単位の機密は `export` / `import` で持ち運ばない。端末を替えても backend の設定と認証で 同じ値が読める - チーム単位のパスへ書けるのは、サーバ側で書き込みのポリシーを付けた利用者だけである。 @@ -494,7 +610,23 @@ cache: 計画の元と現物の間の他人の更新が CAS で止まること、サーバ側が失敗したときに `sources.yml` を確定しないこと、不達で書く前に止まること、`backend: age` への import (`tests/cli/test_env_bundle_backend.py`) -- 実サーバに対する `devbase env backend test` / `devbase up` は手動確認。ポリシーによる +- `store_for` / `release_store` の規則(同一性・`root` の変更・解放後の作り直し・明示した + `store` を控えない)(`tests/env/test_runtime_store.py`) +- `up` の 3 経路の往復回数、`_ensure_env_files` が取得を足さないこと、`env init` の後に + 書いた値で起動すること(`tests/cli/test_up_roundtrips.py`) +- TUI の委譲の入口で控えを捨て、`env edit` → `up` が新しい値で起動すること + (`tests/cli/tui/test_dispatch.py`) +- `issue_token` がログインを増やさないこと・期限前の取り直し・拒否(`tests/env/test_openbao.py`) +- token の書き込みの形(stdin だけで渡す、`mktemp` → `mv -f`、失敗の扱い) + (`tests/env/test_container_token.py`) +- `up` / `scale` が `BAO_ADDR` を足し token を書くこと、ファイル backend で何も足さないこと、 + 書けなくても `up` が失敗しないこと(`tests/commands/test_container_bao.py`) +- `env token` の処理の順、dev サービスの絞り込み、下位ディレクトリからの dev サービス名、 + 接続先の適用、`--print`(`tests/commands/test_env_token.py`) +- base イメージの `bao` の版・両アーキテクチャ・チェックサムの検証の文言 + (`tests/containers/test_base_dockerfile_bao.py`) +- 実サーバに対する `devbase env backend test` / `devbase up` は手動確認。コンテナの中の + `bao kv get` / `patch`、チーム共通への書き込みの 403、`env token` での取り直しも同じ。ポリシーによる 他人のパスと `users/` 直下の一覧の拒否、端末 1 台の `secret_id` の失効も同じ(開発モードの サーバでの確認結果は #166。本番のサーバでは配布後に確かめる) @@ -504,8 +636,10 @@ cache: - [環境変数の暗号化](../user/env-encryption.md) - [CLI リファレンス: env](../user/cli-reference/03-env.md) - 発端の依頼: `issues/security-key.md` -- 実装 PR: devbasex/devbase#171(Infisical 版 #167 を置き換え) +- 実装 PR: devbasex/devbase#171(Infisical 版 #167 を置き換え)、#177(`up` の往復、#168)、 + #178(コンテナの `bao`、#169) - Infisical から OpenBao への切り替えの経緯: devbasex/devbase#166 - [OpenBao: KV v2 API](https://openbao.org/api-docs/secret/kv/kv-v2/) - [OpenBao: AppRole auth](https://openbao.org/docs/auth/approle/) - [OpenBao: Policies](https://openbao.org/docs/concepts/policies/) +- [OpenBao: releases](https://github.com/openbao/openbao/releases) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md deleted file mode 100644 index 8b643191..00000000 --- a/issues/PLAN54_bao-in-container-design.md +++ /dev/null @@ -1,364 +0,0 @@ -# #169: base イメージに `bao` を入れ、起動中のコンテナから機密を取得・変更できるようにする(設計) - -要求と受け入れ条件は `issues/PLAN54_bao-in-container.md` にある。この文書は「どう作るか」だけを扱う。 - -作るものは 3 つである。base イメージの `bao` 2.6.2、`devbase up` がコンテナへ渡す -`BAO_ADDR` と `~/.vault-token`、token を取り直す `devbase env token`。コンテナに置く資格情報は -1 時間で切れる token だけで、`secret_id` はホストから出ない(決定 1・2)。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | dev コンテナの中で `bao` 2.6.2 が使える | 開発者、管理者(carmo-cdk の管理スクリプト) | -| F2 | `devbase up` が、backend が `openbao` のとき、コンテナへ接続先と token を渡す | 開発者(意識せずに使う) | -| F3 | `devbase env token` で、起動中のコンテナの token を再起動せずに取り直す | 開発者(token が切れたとき) | -| F4 | コンテナの中から自分の機密を読む・足す・変える・消す手順の案内 | 開発者 | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| `containers/base/Dockerfile`(変える) | `bao` の tar.gz を取得し、`checksums.txt` で検証して `/usr/local/bin/bao` へ置く。版は `ARG BAO_VERSION` | -| `env/openbao.py` `OpenBaoBackend.issue_token()`(足す) | 現在の token を返す。無い・期限が近ければログインし直す。既存の `_ensure_token` を公開する薄い入口 | -| `env/container_token.py`(新設) | 受け取った token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。replica ごとの繰り返しを持つ。token の取得と backend の判定は持たない(呼び出し側の責務) | -| `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。`issue_token()` で token を得て `push()` へ渡す。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | -| `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | -| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(プロジェクトの解決 → プロジェクト直下の `env` から dev サービス名の解決 → 接続先の解決 → `docker ps` で対象の解決 → `issue_token()` → `push()`。token はコンテナが見つかってから取る)。`--print` は標準出力へ token だけを出す | -| `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`--context` は `env exec` と同じ `_add_context_arg` で足す。`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | -| `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | -| `docs/specifications/secret-backend.md`(変える。実装 PR のマージ後に `plan-to-spec` で。要求仕様の対象範囲の最後の項目) | 確定仕様の追記先。節ごとに足すもの: 「構成要素」の表に `env/container_token.py` の行、図に上の 2 本の辺(`bao` → サーバ、ホスト → コンテナの `~/.vault-token`)。「OpenBao との契約」の「token は実行のたびに取り直し、ディスクへ保存しない」を「ホストでは保存せず、コンテナへは `docker exec` の stdin で渡して `~/.vault-token`(`0600`)に置く」へ改め、`BAO_ADDR`(compose の `environment`、リテラル)と `~/.vault-token`(`mktemp` → `mv -f`、token は argv に出さない)の入出力の契約を箇条書きで足す(#168(PLAN55)も同じ節へ足す予定なので、節を新設せず箇条書きを増やす)。「`devbase env backend`」の次に「`devbase env token`」の節(引数・処理の順・終了コード。この文書の「入出力の契約」を移す)。「セキュリティ」に「コンテナに置く資格情報は 1 時間の token だけで `secret_id` はホストから出ない。token を環境変数にしない理由」(決定 1)。「運用」に token の期限と `devbase env token` での取り直し。「テスト観点」にこの表の新設テスト 4 本 | -| `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | -| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・`umask`・`mktemp` した一時ファイルからの `mv`)と、replica の繰り返し | -| `tests/commands/test_container_bao.py`(新設) | `up` が `BAO_ADDR` を足す/足さない、`_push_bao_token` の要否 | -| `tests/commands/test_env_token.py`(新設) | `env token` の `--print` と既定の経路、backend が `openbao` でないときの失敗、`project.local.yml` の `docker.context` が `docker ps` / `docker exec` の両方に効くこと | - -構成要素の関係: - -```mermaid -graph LR - subgraph host [ホスト: devbase] - UP[commands/container.cmd_up] - TOKCMD[commands/env.cmd_env_token] - PUSH[env/container_token.push] - OB[env/openbao.OpenBaoBackend] - CFG[secrets/backend.yml] - BOOT[secrets/bootstrap.env.age] - end - subgraph container [dev コンテナ] - TF["~/.vault-token (0600)"] - ENVV[BAO_ADDR] - BAO[bao CLI 2.6.2] - end - SRV[(OpenBao サーバ)] - UP -->|environment に BAO_ADDR| ENVV - UP -->|issue_token| OB - TOKCMD -->|issue_token| OB - UP -->|push token| PUSH - TOKCMD -->|push token| PUSH - OB -->|approle login| SRV - OB --> CFG - OB --> BOOT - PUSH -->|docker exec: 一時ファイル → mv ~/.vault-token| TF - BAO --> TF - BAO --> ENVV - BAO -->|kv get / put / patch| SRV -``` - -システムの文脈と配置は上の図の `host` / `container` / `SRV` の 3 つである。変更前と同じで、 -`docs/specifications/secret-backend.md`「構成要素」が持つ。この変更が足す辺は 2 本である。 -コンテナからサーバへ向かう辺(`bao` → `SRV`)と、ホストからコンテナへ token を届ける辺。 - -## 構造 - -```mermaid -classDiagram - class OpenBaoBackend { - +login() - +issue_token() str - -_ensure_token() str - -_token: str - -_token_expires_at: float - } - class ContainerTokenPusher { - +push(container_names: list, token: str) list - -_write_token_file(container_name, token) bool - } - class SecretStore { - +backend_name: str - +config: BackendConfig - -_selected_backend() SecretBackend - } - class TokenCaller { - <<呼び出し側>> - _push_bao_token() - cmd_env_token() - } - TokenCaller ..> SecretStore : backend が openbao か - TokenCaller ..> OpenBaoBackend : issue_token() - TokenCaller ..> ContainerTokenPusher : push(names, token) -``` - -責務の境界は「token を得る」と「token を届ける」で分ける。`_push_bao_token` と `cmd_env_token` -(図の `TokenCaller`)が backend を判定し、`issue_token()` で token を得て `push()` へ渡す。 -`ContainerTokenPusher` は `docker` しか知らず、`OpenBaoBackend` にも `SecretStore` にも依存しない -(テストは token の文字列を渡すだけで済む)。 - -`issue_token()` は `_ensure_token()` をそのまま返す。ログイン失敗の例外(`SecretAuthError` / -`SecretUnreachableError`)はそのまま上へ伝える。`push()` は書けたコンテナ名の一覧を返し、 -書けなかったものは警告を 1 行ずつ出す(`up` の後処理では失敗を握り、`env token` では非ゼロで -終える)。 - -## 入出力の契約 - -### コンテナの中の環境 - -| 名前 | 形 | 出所 | いつ | -| --- | --- | --- | --- | -| `BAO_ADDR` | 環境変数。`backend.yml` の `openbao.url` | `_generate_compose_for` の `dev_environment` | `up` のとき。backend が `openbao` のときだけ | -| `~/.vault-token` | ファイル `0600`、token の文字列 1 行(改行なし) | `_push_bao_token` / `env token` | `up` の [5/6] の後と、`env token` を打ったとき | - -`bao` は `BAO_TOKEN` が無いとき `~/.vault-token`(`BAO_TOKEN_PATH` で変更可)を読む。 -`BAO_TOKEN` を環境変数にしないのは、`docker inspect` と子プロセスの環境に残るためである -(決定 1)。 - -### `devbase env token` - -```text -devbase env token [--print] [--context NAME] -``` - -| 引数 | 意味 | -| --- | --- | -| (なし) | 現在地のプロジェクトの起動中の dev コンテナすべての `~/.vault-token` を書き換える | -| `--print` | コンテナへ書かず、token を標準出力へ 1 行で出す。手で貼りたいとき・別の経路のコンテナのため。プロジェクトとコンテナは見ない | -| `--context NAME` | docker context を一時的に上書きする。`env exec` と同じ `_add_context_arg`(PLAN52) | - -対象のプロジェクトは、他の `env` サブコマンド(`set -p` など)と同じく実行時のディレクトリから -決める(`_current_project_name`)。プロジェクト名を取る引数は置かない。既存の `-p` は名前を -取らない真偽フラグで(`docs/specifications/secret-backend.md`「参照の持ち主と `--user`」)、 -`env token` だけ引数を取る `-p` にすると意味が割れる。別のプロジェクトへ届けたいときは -そのディレクトリで打つ。 - -処理の順は「backend の判定 → プロジェクトの解決 → dev サービス名の解決 → 接続先の解決 → -対象の解決 → ログイン(`issue_token()`)→ 書き込み」で、`--print` は backend の判定の後すぐログインして token を出す -(プロジェクトもコンテナも見ない)。ログインを対象の解決より**後**に置くのは、届け先が無い -(`projects/` の下ではない、起動中の dev コンテナが無い)ときにサーバへ token を発行させない -ためである。発行した token は使われないまま 1 時間サーバに残る。サーバへ届かない端末では -「プロジェクトの外で打った」より先に「サーバへ到達できない」が出て、本当の誤りが読めない -(round 3 のレビュー)。 - -dev サービス名は、対象の解決より**前**に、解決済みプロジェクトの直下の非機密設定 -`projects//env` から取る。起動ラッパー `bin/devbase` が `source ./env` するのは -実行時のディレクトリの `env` だけなので、`projects//src` などの下位ディレクトリから -打つと `DEV_SERVICE_NAME` が環境に載らず、`get_dev_service_name()` は既定の `dev` を返す。 -`env` に `DEV_SERVICE_NAME=workspace` を置いて起動した `workspace-1` が、`-` の絞り込みで -対象から外れる(round 5 のレビュー)。そこで `_current_project_name` で決めたプロジェクトの -`env` を `_load_project_env`(`commands/container.py`。ラッパーと同じ `KEY=VALUE` の解釈)で -`os.environ` へ載せてから `get_dev_service_name()` を呼ぶ。プロジェクト直下で打ったときは -ラッパーが載せた値と同じ値を載せ直すだけで、結果は変わらない。 - -対象の解決は `docker ps` 1 回で行う。`--filter label=com.docker.compose.project=` で -現在地のプロジェクトに絞り、`--format '{{.Names}}\t{{.Label "com.docker.compose.service"}}'` で -名前とサービス名を取り、サービス名が `-`(`` は上で解決した dev サービス名、`` は -1 以上の整数)のものだけを残す。`up` が生成する `.docker-compose.scale.yml` は dev の各インスタンスを -サービス `{dev}-{i}`・`container_name` `${COMPOSE_PROJECT_NAME}-{dev}-{i}` で定義する -(`volume/compose.py` `_build_scaled_services`)ので、このラベルで dev 以外のサービス(DB、 -`snapshot` など同じプロジェクトのコンテナ)が届け先に入らない。プロジェクト名だけで絞ると -それらにも `docker exec` を打ち、`$HOME` の違いで失敗するか root の home に token を残す。 -`up` の `_push_bao_token` は `docker ps` を使わず、`_apply_window_titles` と同じく -`opener.resolve_container_name(dev, project, index)` を 1..scale で回して名前を組む -(起動直後で scale が分かっている)。 - -接続先(docker context)は `env token` 自身が決める。`up` が `_resolve_docker_target` で当てた -`DOCKER_CONTEXT` はその process の環境にしか無く、後から別の process で打つ `env token` には -残らない。何もしないと、`project.local.yml` の `docker.context` だけでリモート(PLAN52)を -指す端末では既定の daemon を見に行き、コンテナが無いか、同名の別のコンテナへ token を書く。 -そこで `cmd_env_exec` と同じ形で決める。`_current_project_name` で決めたプロジェクトの直下の -`project.local.yml` を `load_project_local_config` で読み、`docker_context.choose_context` -(CLI `--context` > env `DEVBASE_DOCKER_CONTEXT` > ファイル > 未指定)で 1 つに決め、 -`docker_context.apply` で process の環境へ当てる。この後の `docker ps`(対象の解決)と -`docker exec`(書き込み)は同じ接続先へ向かう。gid・home の解決(`_resolve_docker_target`)は -要らない(compose を生成しない)。`--print` はここへ来ない。 - -状況は処理の順に並べる(上の行で止まれば下は見ない)。 - -| 状況 | 出力 | 終了コード | -| --- | --- | --- | -| backend が `openbao` でない | 「backend が openbao ではありません」 | 1 | -| `--print` | token を 1 行(backend の判定とログインの結果以外の条件は見ない) | 0 | -| `projects/` の下ではない | 「プロジェクトのディレクトリで実行してください」(ログインしない) | 1 | -| 起動中の dev コンテナが無い | 「起動中の dev コンテナがありません: 」(ログインしない) | 1 | -| ログインが拒まれた(400 / 403) | 既存の `SecretAuthError` の文言 | 1 | -| 到達できない | 既存の `SecretUnreachableError` の文言(**控えは使わない**。token は控えられない) | 1 | -| 一部のコンテナに書けなかった | 書けたもの・書けなかったものを 1 行ずつ | 1 | -| すべて書けた | 書いたコンテナ名を 1 行ずつ(token は出さない) | 0 | - -`env token` は `_NO_SECRET_INJECTION` に入れる(機密の注入は要らず、注入の往復を増やさない)。 - -### `docker exec` の形 - -```text -docker exec -i sh -c ' - umask 077 - tmp=$(mktemp "$HOME/.vault-token.XXXXXX") || exit 1 - cat > "$tmp" && chmod 0600 "$tmp" && mv -f "$tmp" "$HOME/.vault-token" \ - || { rm -f "$tmp"; exit 1; }' - (stdin: token) -``` - -- `-i` で stdin を渡し、引数に token を載せない(`ps` に出さない) -- 同じディレクトリの一時ファイルへ書いてから `mv -f` で置き換える(`editor/window_title.py` - `_write_command` と同じ形)。`cat >` で直接上書きすると、`env token` の再実行で既存ファイルの - mode がそのまま残り(`umask` は新規作成にしか効かない)、途中で切れると空のファイルが残る -- 一時ファイルは固定名にせず `mktemp` で**毎回新しく**作る。固定名(`.vault-token.tmp`)だと、 - その名前のファイルが既にあるとき `cat >` は既存の inode へ書き、`umask 077` は効かない - (0644 で先に置いておくと、終了コード 0 のまま `~/.vault-token` が 0644 になる。round 2 の - レビューで再現)。並行して打った `env token` 同士が 1 つの一時ファイルを取り合うこともない。 - `mktemp` は `O_EXCL` で `0600` に作るので、`mv` 後の mode は前の状態によらず `0600` になる。 - `chmod 0600` は `mktemp` の実装差への保険で、失敗すれば書き込みを止める。 - `window_title.py` の `cp -p`(既存の mode を写す)は要らない。ここでは前の mode を - 引き継がず、常に `0600` にしたい -- 接続先(`DOCKER_CONTEXT`)は呼び出し側が process の環境へ当てておく。`up` は - `_resolve_docker_target` が当てた同じ process の中で `_push_bao_token` を呼ぶ。`env token` は - 自身で決める(「`devbase env token`」の節)。どちらもリモートの daemon(PLAN52)へ同じ形で届く -- `$HOME` はコンテナの利用者(`ubuntu`)のもの。`docker exec` の既定の利用者は compose の - `user` 設定に従い、base イメージは `USER ubuntu` で終わる - -## 処理の流れ - -```mermaid -sequenceDiagram - participant U as 利用者 - participant UP as cmd_up / cmd_env_token - participant ST as SecretStore/OpenBaoBackend - participant P as container_token.push - participant D as docker - participant C as dev コンテナ - U->>UP: devbase up - UP->>ST: _inject_secrets(既存。ログイン + GET) - UP->>D: compose up(environment に BAO_ADDR) - D->>C: 起動 - UP->>UP: [5/6] ready を待つ - alt backend が openbao(_push_bao_token) - UP->>ST: issue_token()(同じインスタンス。期限内なら再ログインしない) - UP->>P: push([…], token) - P->>D: docker exec -i sh -c '… tmp=$(mktemp …) && cat > $tmp && mv -f $tmp ~/.vault-token' - D->>C: ~/.vault-token を書く - P-->>UP: 書けたコンテナ名(失敗は警告。up は倒さない) - end - UP->>UP: [6/6] エディタ - Note over C: 1 時間後に token が切れる - U->>C: bao kv get … → 403 - U->>UP: devbase env token - UP->>UP: プロジェクトを決め、その直下の env から dev サービス名を取る(下位ディレクトリからでも同じ) - UP->>UP: docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) - UP->>D: docker ps --filter label=com.docker.compose.project=(service が - のものだけ残す) - UP->>ST: issue_token()(新しい token。コンテナが見つかってから) - UP->>P: push([…], token) - P->>D: docker exec -i … - U->>C: bao kv get … → 200 -``` - -`up` の途中で `issue_token()` が返す token は、`_inject_secrets(required=True)` と同じ -インスタンスのものにする。現行の `_inject_secrets` は `runtime.inject(root, project)` を -`store` 引数なしで呼び、戻り値の `SecretEnv` をその場で `_generate_compose_for` へ渡すだけで、 -`SecretStore` は保持していない(`commands/container.py`)。この実装で、`_run_deploy_pipeline` -が `SecretStore` を 1 つ作って `inject(..., store=)` へ渡し、同じものを `_push_bao_token` へも -渡すよう変える(下の表「PLAN55 の前」)。既存コードに再利用できる `SecretStore` があるわけでは -ない。こうして `up` の往復を増やさない(ログインは注入と token で 1 回)。 - -**#168(PLAN55)が `SecretStore` の引き回しを変える場合は、この呼び出しもそちらの経路に乗せる。** - -| 実装の順序 | `SecretStore` の渡し方 | -| --- | --- | -| PLAN55 の後 | PLAN55 の設計に従う | -| PLAN55 の前 | `_run_deploy_pipeline` の中で `SecretStore` を 1 つ作り、注入と token の両方へ渡す | - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| セキュリティ | `secret_id` の置き場所を広げない方式を既定とする | コンテナへ渡すのは 1 時間で切れる token だけ。`secret_id` はホストの `bootstrap.env.age` から出ない。token はファイル `0600` に置き、環境変数と compose ファイルに書かない | コンテナ内で `env \| grep -c SECRET_ID` が 0、`.docker-compose.scale.yml` に `hvs.` が無い、`stat -c %a ~/.vault-token` が 600 | -| セキュリティ | `bao` の導入はチェックサムで検証する | `checksums.txt` を同じリリースから取得し、`grep` で対象行を抜いて `sha256sum -c` | 行を改ざんした Dockerfile でビルドが失敗する(テストは Dockerfile の文言で固定) | -| 運用・保守性 | 版の更新が `ARG` 1 行 | `ARG BAO_VERSION=2.6.2` を 1 か所に置き、URL・ファイル名・検証すべてがそれを参照する。tar.gz には `bao` / `CHANGELOG.md` / `LICENSE` / `README.md` の 4 つが入る(v2.6.2、70 MB)ので `tar -xzf - bao` で 1 つだけ取り出す | `grep -c 2.6.2 containers/base/Dockerfile` が 1 | -| 運用・保守性 | token 切れの症状(403)と対処を `docs/user/` に書く | F4 の節に「`permission denied` が出たら `devbase env token`」を書く | 文書を読む | -| システム環境 | amd64 / arm64 の両方でビルドできる | `dpkg --print-architecture` で `amd64` / `arm64` を選ぶ(session-manager-plugin と同じ書き方) | 両アーキテクチャのホストで `devbase build` | -| システム環境 | backend が `openbao` でない端末は影響を受けない | `BAO_ADDR` の付与と `_push_bao_token` は `store.backend_name == 'openbao'` のときだけ | backend が `age` の `up` で生成される compose が変更前と一致するテスト | - -## 決定の記録 - -### 決定 1: コンテナへ渡す資格情報は 1 時間の token だけにし、`~/.vault-token` に置く - -token は AppRole のログインで得るもので 1 時間で切れる。盗まれても被害は 1 時間で、 -`secret_id`(失効させるまで使える)を置くより狭い。ファイルにするのは、環境変数だと -`docker inspect` と全プロセスの環境に残り、`env` を出力するコマンドや子プロセスに漏れる -ためである。`bao` は `~/.vault-token` を既定で読むので、利用者は何も設定しなくてよい。 - -`role_id` / `secret_id` をコンテナへ渡して `bao write auth/approle/login` で取り直す案は採らない。 -取り直しは楽になる。しかしコンテナの中で動く AI CLI と npm のパッケージが、ホストと同じ -資格情報を永続的に持つ形になる。PLAN51 が `secret_id` を age で守った意味が無くなる。 - -### 決定 2: token の取り直しはホストの `devbase env token` が行い、コンテナには資格情報を置かない - -コンテナの中から取り直す手段を持たせるには、コンテナに資格情報(決定 1 で退けた)か、 -人のログイン(OIDC)が要る。OIDC の `bao login -method=oidc role=google` は、ブラウザの戻り先がコンテナの -`localhost:8250` に届く必要がある。これは VS Code のポート転送に依存する。devbase が作る -ものではなく、文書に「別の経路」として書くだけにする。 - -ホストで取り直す形なら、`devbase up` が既に持つ資格情報と経路をそのまま使える。1 時間ごとの -手間は、コンテナの中で `bao` を打つ頻度(キーを足す・値を見るとき)に対して十分に小さい。 -起動時の注入で足りる日常の利用では、取り直しは一度も要らない。 - -### 決定 3: `BAO_ADDR` は環境変数で渡し、`docker exec` で書かない - -接続先は機密ではなく、compose の `environment` にリテラルで書ける(`DEVBASE_ACCOUNT_GROUP` と -同じ扱い)。ファイルにする token と分けることで、token の書き込みに失敗しても `bao` は「token が無い」と -分かる。`BAO_ADDR` まで無いと誤りが「接続先が無い」に変わり、原因が読めない。 - -### 決定 4: token の書き込みは `up` の [5/6] の後に置き、失敗しても `up` を倒さない - -コンテナが ready になる前は `docker exec` が失敗する。`_apply_window_titles` と同じ位置で、 -同じく付随処理として扱う。token が書けなくてもコンテナは起動済みの環境変数で動くため、 -`up` を失敗にすると本来の目的(開発環境の起動)を損なう。失敗は警告で伝え、利用者は -`devbase env token` でやり直せる。 - -### 決定 5: `bao` は tar.gz を `checksums.txt` で検証して入れ、`.deb` は使わない - -`.deb` は依存の解決と postinst(`openbao` システムユーザーの作成、systemd ユニット)を伴い、 -CLI だけが要る base イメージには余計である。tar.gz は `bao` バイナリ 1 つで、配置先を -`/usr/local/bin` に固定できる。署名(`.gpgsig` / `.sigstore.json`)の検証は、base イメージの -他のツール(AWS CLI・gcloud・uv)も行っていないため揃える。チェックサムは同じリリースから -取るので、改ざんに対する防御は「GitHub Releases が一貫している」前提に依る。 - -### 決定 6: 全端末の base イメージに `bao` を入れ、backend で入れ分けない - -管理者の作業(carmo-cdk の `openbao-admin.sh`)は `bao` に依存し、管理者の端末の backend -設定とは無関係である。入れ分けると、backend を `openbao` にしていない管理者の端末で -`bao` が無い。サイズの増分は 1 バイナリ(tar.gz で 70 MB。展開後の実測は実装時に CHANGELOG へ書く)で、 -既に gcloud SDK と Playwright を持つイメージに対して許容する。 - -## テスト設計 - -| 受け入れ条件(PLAN54) | 何で確かめるか | -| --- | --- | -| 1. `bao version` が `OpenBao v2.6.2` | 手動(`devbase build` → `docker run --rm devbase-base:latest bao version`)。amd64 は CI 相当のホスト、arm64 はこの Mac | -| 2. チェックサム不一致でビルドが失敗する | `tests/containers/test_base_dockerfile_bao.py`: Dockerfile に `sha256sum -c` の行と `checksums.txt` の取得があることを固定。実ビルドの失敗は実装時に 1 度手で確かめる | -| 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | -| 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | -| 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | -| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `docker ps` → `issue_token` → `push` の順、`projects/` の外と起動中の dev コンテナが無いときに `issue_token` を呼ばないこと、`docker ps` の結果から service が `-` でないコンテナ(DB・`snapshot`)を外すこと、`env` に `DEV_SERVICE_NAME=workspace` を持つプロジェクトの `projects//src`(下位ディレクトリ)を CWD にし `DEV_SERVICE_NAME` を環境に載せない状態で、`workspace-1` が対象に残り `docker exec` が呼ばれること(プロジェクト直下の `env` から dev サービス名を取る条件の検証)、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | -| 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | -| 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い。シェルの文言に `mktemp` と `umask 077` があり、固定名の一時ファイルが無い | -| 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | -| 10. Dockerfile の導入行を固定するテスト | `tests/containers/test_base_dockerfile_bao.py`(版・`amd64` / `arm64` の分岐・`sha256sum -c`) | - -成果物のうち `docs/specifications/secret-backend.md` の追記は受け入れ条件を持たず、テストも無い。`plan-to-spec` のとき、構成要素の表の同じ行に書いた節の一覧と追記後の spec の見出しを突き合わせる。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| `bao` が `~/.vault-token` を既定で読むこと | バイナリの文字列(`~/.vault-token` / `BAO_TOKEN_PATH`)と OpenBao の CLI 文書から読んだ。実サーバで確かめるのはリリース後テスト。読まなければ `BAO_TOKEN_PATH` を `environment` で指す | -| 派生イメージが `USER` を変えていないこと | `containers/*/Dockerfile` は 10 本。dev イメージは base と派生 8 本の 9 本で、`USER` を書く 8 本(base と派生 7 本)は `ubuntu`(`${USERNAME}`)で終わり、`general` は `USER` を書かず base の `ubuntu` を継ぐ(2026-09-14 に数えた)。`snapshot` は `FROM ubuntu:26.04` で `USER` が無く root だが、dev サービスではないので token の届け先に入らない(対象は起動中の dev コンテナだけ)。利用者側の `projects/*/compose.yml` が `user:` を変える場合は `$HOME` が変わり、その場合は `BAO_TOKEN_PATH` で指す運用になる | -| PLAN55(#168)との順序 | 先に入った側に合わせて `SecretStore` の引き回しを決める(処理の流れの節) | diff --git a/issues/PLAN54_bao-in-container.md b/issues/PLAN54_bao-in-container.md deleted file mode 100644 index e9a1a9c0..00000000 --- a/issues/PLAN54_bao-in-container.md +++ /dev/null @@ -1,192 +0,0 @@ -# PLAN54: base イメージに `bao` を入れ、起動中のコンテナから機密を取得・変更できるようにする - -- 発端: #169 -- ワークフローモード: `standard` - - 根拠: base イメージ(`containers/base`)と、コンテナへ接続情報を渡す起動経路 - (`lib/devbase`)の 2 領域にまたがる本番の振る舞いの追加。公開インタフェース - (コンテナ内で使える環境変数)が増える -- 閉じる課題: #169 - -## 依頼(原文) - -> base イメージに OpenBao の CLI(`bao`)を入れる。そのうえで、**起動中のコンテナから、再起動せずに機密の変数を取得・変更できる**ようにする。 - -> | # | 内容 | -> | --- | --- | -> | 1 | base イメージに `bao` を入れる。版はサーバと同じ 2.6 系で固定する(2026-09-13 時点の最新は 2.6.2) | -> | 2 | 起動中のコンテナから、再起動せずに自分の機密を取得できる | -> | 3 | 起動中のコンテナから、再起動せずに自分の機密を変更できる(キーの追加・値の変更・キーの削除) | - -> 管理者の作業(利用者の登録、端末の資格情報の発行と失効)も、base コンテナの `bao` で行う前提にする。サーバ側の管理スクリプトは `bao` を使う。 - -コメント(2026-09-14): - -> 着手時は「手元キャッシュとの整合」の論点を、確定仕様の「控えは読み取り専用(set / delete / edit は fetch で現物を読む)」の規則と突き合わせること。 - -## 目的 - -- 起動中の dev コンテナの中で `bao` が使え、再起動せずに自分の機密(`users//…`)を - 読み書きできる -- サーバと同じ 2.6 系の `bao` を、amd64 / arm64 の両方で、検証付きで入れる -- 管理者が base コンテナから `bao` で管理操作(carmo-cdk の `openbao-admin.sh`)を行える - -## 前提 - -- 前提 1: backend が `openbao` でない端末でも base イメージに `bao` を入れる。サーバ側の - 運用リポジトリの管理スクリプトが `bao` に依存するためである。設定で入れ分けると、管理者の - 端末で `bao` が無い状態が起きる。 - 成否の判定: `containers/base/Dockerfile` に `bao` の導入が条件なしで書かれている -- 前提 2: 版は `2.6.2` に固定し、Dockerfile の `ARG` で持つ。GitHub Releases の - `checksums.txt` で検証する(署名の検証はしない。base イメージの他のツールも署名は - 見ていない)。成否の判定: 版を変えるのが `ARG` 1 行で済む -- 前提 3: コンテナの中で `bao` が接続に使うのは `BAO_ADDR`(環境変数)と token の 2 つで、 - `secrets/backend.yml` を読ませない(コンテナは `$DEVBASE_ROOT` を持たない)。 - ~~token も環境変数 `BAO_TOKEN`~~ → token はファイル `~/.vault-token` に置く(2026-09-14、 - 設計の決定 1。環境変数だと `docker inspect` と子プロセスに残るため)。 - 成否の判定: コンテナ内で `env | grep ^BAO_` が `BAO_ADDR` だけを返し、`~/.vault-token` が `0600` で存在する -- 前提 4: token は 1 時間で切れる(サーバの `token_max_ttl=1h`)。起動中のコンテナから - 取り直す手段が要る。**どの資格情報でどう取り直すか**は設計工程で決める(下の未決)。 - 成否の判定: 設計文書の「決定の記録」にこの決定がある -- 前提 5: コンテナの `bao` で書き込んだ変更は、ホスト側の控え(`secrets/cache/`)には - 反映しない。控えは読み取り専用で、`set` / `delete` / `edit` は `fetch` で現物を読む - (`docs/specifications/secret-backend.md`「失敗の種類とキャッシュ」)。次の `devbase up` が - 現物を読めば整合する。古い値になるのは不達時に控えから起動したときだけで、その旨は既存の - 警告が出る。成否の判定: ホスト側のコードにコンテナからの通知を受ける経路が無い -- 前提 6: 起動済みのプロセスの環境変数は変えられない。コンテナの中での「変更」は - OpenBao 側の値の変更を指し、シェルへ読み直す手段は**案内**(`docs/user/`)で示す - (例: `export KEY="$(bao kv get -field=KEY …)"`)。成否の判定: 起動中のプロセスの - 環境を書き換える仕組みを作らない -- 前提 7: backend が `openbao` でない端末(`age` / `plaintext`)では、~~`BAO_ADDR` / - `BAO_TOKEN`~~ → `BAO_ADDR`(環境変数)と token(`~/.vault-token`)をコンテナへ渡さず - (2026-09-14、決定 1 に揃えた)、それ以外の振る舞いは変えない(PLAN51 の前提 3) - -## 対象範囲 - -含む: - -- `containers/base/Dockerfile` への `bao` 2.6.2 の導入(amd64 / arm64、checksums.txt で検証) -- backend が `openbao` のとき、`devbase up` がコンテナへ `BAO_ADDR`(環境変数)と、有効な token を - 書いた `~/.vault-token` を渡すこと(~~`BAO_ADDR` と有効な `BAO_TOKEN`~~ 2026-09-14、決定 1 に揃えた) -- 起動中のコンテナから token を取り直す手段(方式は設計で決める) -- 利用者への案内(`docs/user/env-backend.md`): パスの形、`kv put` と `kv patch` の違い、 - シェルへの読み直し、token の期限と取り直し -- `docs/specifications/secret-backend.md` への確定仕様の追記(`plan-to-spec`) - -含まない: - -- 起動中のプロセスの環境変数を書き換えること -- コンテナからの書き込みをホスト側の控えへ反映すること(前提 5) -- チーム共通(`team/…`)への書き込み(サーバのポリシーが `devbase-team-writer` に限る。 - `bao` はそのまま使えるため devbase 側で作るものは無い) -- `devbase up` の 2 度注入の解消(#168、PLAN55) -- 管理スクリプト(carmo-cdk `bin/openbao-admin.sh`)の変更 - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| `bao` | OpenBao の CLI。接続先は環境変数 `BAO_ADDR` から、token は環境変数 `BAO_TOKEN` が無ければ `~/.vault-token` から読む | -| token | AppRole ログインで得る `client_token`。TTL 1 時間、延長不可 | -| 端末の資格情報 | AppRole の `role_id` / `secret_id`。ホストの `secrets/bootstrap.env.age` にある | -| 控え | ホスト側 `secrets/cache/` の age 暗号化キャッシュ。読み取り専用 | - -## 受け入れ条件 - -- [ ] `devbase build`(base イメージ)の後、`docker run --rm devbase-base:latest bao version` が - `OpenBao v2.6.2` を含む行を出す。amd64 と arm64 のどちらのホストでも同じ -- [ ] ダウンロードした tar.gz の SHA-256 が `checksums.txt` と一致しないとき、イメージの - ビルドが失敗する(`Dockerfile` の該当行を読み、`sha256sum -c` 相当の検証があること - で判定する) -- [ ] 前提: backend が `openbao` で `devbase env backend test` が通る - 操作: `devbase up` の後に `docker exec bash -lc 'bao kv get -mount=devbase -field= users//global'` - 結果: ホストの `devbase env get --user ` と同じ値を返す -- [ ] 前提: 同上 - 操作: コンテナ内で `bao kv patch -mount=devbase users//global NEW_KEY=v1`、 - 続けてホストで `devbase env get --user NEW_KEY` - 結果: `v1`(ホスト側は控えではなく現物を読む) -- [ ] 前提: 同上 - 操作: コンテナ内で `bao kv get -mount=devbase team/global` と - `bao kv put -mount=devbase team/global X=1` - 結果: 前者は成功、後者は 403 で終了コード 2(devbase 側で何も作らない。サーバの - ポリシーの確認) -- [ ] 前提: コンテナ起動から 1 時間以上経ち、~~`BAO_TOKEN`~~ → `~/.vault-token` の token が - 切れている(2026-09-14、決定 1 に揃えた) - 操作: 設計で決めた取り直しの手段を実行してから `bao kv get …` - 結果: 再起動せずに値を読める -- [ ] backend が `age` の端末で `devbase up` したとき、コンテナの `env` に `BAO_ADDR` が無く、 - `~/.vault-token` が作られず、生成される `.docker-compose.scale.yml` が変更前と同じ -- [ ] token の値がログ(`devbase --verbose up` の出力)と `.docker-compose.scale.yml` と - `docker inspect` の `Env` に書かれない(~~compose には変数名だけを列挙する~~ → - token は `docker exec` の stdin で渡す。2026-09-14、設計の決定 1) -- [ ] `uv run pytest tests/` が全件通り、`ruff check lib` と `shellcheck` が変更前と同じ結果 -- [ ] `tests/containers/` に `Dockerfile` の `bao` の導入行を固定するテストがある - (版・アーキテクチャ・検証の 3 点) - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| セキュリティ | `secret_id` の置き場所を広げない方式を既定とする(設計で判断し、広げる方式を採るなら「決定の記録」に理由を書く)。token は compose ファイルへ書かない。`bao` の導入はチェックサムで検証する | -| 運用・保守性 | 版の更新が `ARG` 1 行。token 切れのときの症状(403)と対処を `docs/user/` に書く | -| システム環境 | amd64 / arm64 の両方でビルドできる。backend が `openbao` でない端末は影響を受けない | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | コンテナ内の環境変数 `BAO_ADDR` とファイル `~/.vault-token` が増える(backend が `openbao` のときだけ)。ホストのコマンド `devbase env token` が増える | -| データ | スキーマ変更なし。`secrets/backend.yml` の項目が増えるかは設計で決める | -| 既存の振る舞い | base イメージに `bao` が入る(全端末)。`openbao` の端末では `devbase up` がコンテナへ環境変数 `BAO_ADDR` を追加で渡し、`~/.vault-token` を書く | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| 起動 | `devbase build` → `devbase up ` | -| テスト | `uv run pytest tests/` | -| 静的解析 | `ruff check lib`、`shellcheck --severity=error bin/devbase install.sh`、`python -m compileall -q lib bin` | -| 手動確認 | 利用者の端末(backend `openbao`、PLAN53 の後)で受け入れ条件 3〜6 を実行する。リリース後テストの工程で行う | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | `docs/developer/architecture.md`。base イメージは `containers/base/`、起動経路は `lib/devbase/commands/container.py` と `lib/devbase/project/runtime.py`(`container_env`)、OpenBao の HTTP は `lib/devbase/env/openbao.py` | -| コーディング規約 | `docs/developer/contributing.md`。CI は `compileall` / `ruff` / `shellcheck` | -| テスト戦略 | `tests/containers/` で Dockerfile とエントリポイントの文言を固定、`tests/env/test_openbao.py` の偽サーバで HTTP の往復を固定、`tests/commands/` で `up` が渡す環境変数を固定。実サーバへの接続は手動確認 | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、`ruff`、`shellcheck`。`bao` の版は `ARG` で持つ | -| 確認してから行う | 端末の `secret_id` をコンテナへ渡す方式の採用。`secrets/backend.yml` の項目の追加。設計 Pull Request のマージ | -| 行わない | 起動中のプロセスの環境変数の書き換え。ホスト側の控えへの反映。管理スクリプトの変更 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| ~~token を取り直す手段~~ → 決まった: ホストの `devbase env token` が起動中のコンテナの `~/.vault-token` を書き換える(設計の決定 2。(b) `secret_id` をコンテナへ渡す案と (c) OIDC 案は採らない) | 設計 Pull Request のマージで利用者が承認する | 設計 | - -## 実装計画 - -設計は `issues/PLAN54_bao-in-container-design.md`(マージ済み #175)。PLAN55 (#177) が先に入ったため、 -`_push_bao_token` の token は `runtime.store_for(root)`(注入と同じ `SecretStore`)から取る -(設計「処理の流れ」の表「PLAN55 の後」)。 - -| Task | 対象ファイル | 変更内容 | 満たす受け入れ条件 | 進め方 | -| --- | --- | --- | --- | --- | -| 1 | `containers/base/Dockerfile`、`tests/containers/test_base_dockerfile_bao.py` | `ARG BAO_VERSION=2.6.2`、tar.gz + `checksums.txt` を取得し `sha256sum -c`、`bao` だけを `/usr/local/bin` へ | 1・2・10 | 文言を固定するテスト → Dockerfile。実ビルドは手で 1 度 | -| 2 | `lib/devbase/env/openbao.py`、`tests/env/test_openbao.py` | `issue_token()`(期限内なら再ログインしない) | 6 の土台 | 偽サーバで login 回数を固定 → 実装 | -| 3 | `lib/devbase/env/container_token.py`、`tests/env/test_container_token.py` | `push(names, token, runner=)`: `docker exec -i` + `mktemp` → `mv -f`、token は stdin のみ | 8 | runner のスタブで argv / input / 文言を固定 → 実装 | -| 4 | `lib/devbase/commands/container.py`、`tests/commands/test_container_bao.py` | openbao のとき `dev_environment` に `BAO_ADDR`、[5/6] の後に `_push_bao_token`(失敗は警告) | 7 | up の harness で compose 引数と docker exec の有無を固定 → 実装 | -| 5 | `lib/devbase/commands/env.py`、`lib/devbase/cli.py`、`tests/commands/test_env_token.py` | `devbase env token [--print] [--context NAME]`、`SUBCMD_MAP`、`_NO_SECRET_INJECTION` | 6 | 設計の状況表の行ごとにテスト → 実装 | -| 6 | `docs/user/env-backend.md` | 「コンテナの中から `bao` を使う」の節 | F4 | 文書 | - -設計からの追加(実装で決めたこと): `cmd_scale` も構成を作り直すため、`up` と同じく `BAO_ADDR` を足し、 -増やしたインスタンス(`current_scale + 1`〜)へ token を書く(2026-09-14。設計は `up` だけを挙げていたが、 -`scale` で増えたコンテナに `BAO_ADDR` と token が無い状態を作らないため)。 - -リスク: `container.py` は 1300 行超。触るのは `_run_deploy_pipeline` と `cmd_up` の後処理の数行に限る。 -切り戻し: 差分を戻すだけ(永続データなし)。イメージは再ビルドで元に戻る。 diff --git a/issues/PLAN55_up-single-injection-design.md b/issues/PLAN55_up-single-injection-design.md deleted file mode 100644 index d67686d7..00000000 --- a/issues/PLAN55_up-single-injection-design.md +++ /dev/null @@ -1,279 +0,0 @@ -# #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 回の往復回数がテストで固定される | 保守する人 | -| F4 | 共通機密が未作成で `env init` を走らせた `up` は、`env init` が書いた変数でコンテナを起動する(従来どおり) | 開発者(初回の `up`) | -| F5 | TUI で機密を書いて(`env edit` など)から `up` すると、書いた値でコンテナを起動する(従来どおり) | 開発者(TUI) | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| `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)` に置き換える。子プロセスの `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 回あたり」の文を、持ち回りの規則とともに確定仕様にする | - -構成要素の関係: - -```mermaid -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] - 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 - TD -.入口で捨てる.-> RS - TD --> DL - DL --> INJ - DL -->|finally| RS - INJ --> RES - ENS --> SF - ENS -.env init の後.-> RS - 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 × 1〜2(_ensure_env_files。プロジェクト側はローカル .env が無いときだけ) - 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 | -| `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 プロセスで操作を続ける)では、委譲の入口(`tui/dispatch.py` の `_preserve_cwd_env`)で -控えを捨てる。`_load_secret_env` が起動時に作った `SecretStore` は最初の操作にも引き継がず、 -毎回 `_inject_secrets` が作り直して現物を読む。**TUI の中で機密を書いてから `up` しても、書いた -値で起動する**(決定 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 は操作の入口で控えを捨て、操作ごとに現物を読む - -~~決定 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 対象範囲)。 - -決定 2 の「入口で捨てない」は `commands/container.py` の `_dispatch_lifecycle`(CLI と TUI の -共有)についての判断で、`_load_secret_env` の控えを CLI が使えるようにするためである。 -`tui/dispatch.py` は TUI だけが通るので、そこで捨てても CLI の往復は変わらない。 - -起動からの経過時間で捨てる案は引き続き採らない。境界の値を決める根拠が無く、テストで時刻を -偽る手間が増える。 - -### 決定 4: `_ensure_env_files` の存在判定の意味は変えない - -`exists()` の意味(ファイル backend はファイルの有無、`openbao` は取得した内容が空でない)は -そのままにする。持ち回った `SecretStore` に置き換えるだけで、`openbao` では `_seen` から -返るので往復が消える。 - -注入済みの `SecretEnv.global_names` で判定する案(#168 の案の 2 つ目)は採らない。age の -空ファイルは今日「存在する」と判定されるが、`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) | 何で確かめるか | -| --- | --- | -| 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` | -| 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` 変更・解放) | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| `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 deleted file mode 100644 index d2e9ab5a..00000000 --- a/issues/PLAN55_up-single-injection.md +++ /dev/null @@ -1,254 +0,0 @@ -# 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` / `tests/cli/tui/test_dispatch.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` × 1〜2(共通は常に、プロジェクトはローカル `.env` が無いときだけ。同じインスタンス内なので GET は参照ごとに 1 回) | 1 | 1〜2(ローカル `.env` が無ければ 2) | -| `container._run_deploy_pipeline` | `_inject_secrets(required=True)` | 1 | 4 | - -合計: 認証 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` 自身が `env init` で書いた分は読み直す。2026-09-14、 - 前提 5)。 - 成否の判定: 受け入れ条件 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 -- 前提 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 - -## 対象範囲 - -含む: - -- `cli._load_secret_env` → `container` の各経路で `SecretStore` と解決結果を引き継ぐ仕組み -- ~~`_ensure_env_files` の存在判定を注入済みの結果で行うこと~~ → `_ensure_env_files` の - 存在判定で注入と同じ `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`) - -含まない: - -- `down` / `logs` / `ps` など `required=False` の経路の往復(`up` ほど多くない。数えて - 仕様を超えていれば範囲外として起票する) -- `runtime.resolve()` の重ね順・`SecretStore` の HTTP の契約の変更 -- TUI(1 プロセスで複数の操作を続ける経路)の往復。`_dispatch_lifecycle` の入口で - `docker_context.reset()` と同じ扱いにするかは設計で決めるが、TUI の往復数は条件にしない - (決まった: TUI の委譲層 `tui/dispatch.py` の入口で捨てる。2026-09-14、設計の決定 3) -- コンテナへの `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` - が変更前と同じ結果 -- [ ] 前提: 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) -- [ ] 前提: 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) - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | `up` 1 回の往復が認証 1 回 + 参照ごとに 1 回(実サーバの実測は 4 参照で 369〜392 ms。往復が半分以下になることを実機で確かめる) | -| 運用・保守性 | 往復回数を偽サーバのテストで固定し、経路を足したときに増えたことが分かる | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わらない | -| データ | 変わらない | -| 既存の振る舞い | `up` の途中で 2 度目の解決をしなくなる。`_ensure_env_files` がサーバへ問い合わせなくなる。共通機密が未作成で `env init` を走らせたときだけ、書いた後に読み直す(その `up` に限り認証 1 回 + GET 4 回が足される)。TUI は操作の入口で控えを捨て、起動時に読んだ値を最初の操作にも引き継がない(TUI の操作 1 回あたり認証 1 回 + GET 4 回。今日より少ない) | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `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 のマージで利用者が承認する | 設計 | - -## 実装計画 - -設計は `issues/PLAN55_up-single-injection-design.md`(マージ済み #176)。タスクは設計の -「構成要素」の行から導く。1 タスクが独立して検証できる単位にし、失敗するテスト → 最小実装 → -整理の順で進める。 - -### 修正対象 - -- `lib/devbase/env/runtime.py`、`lib/devbase/commands/container.py`、`lib/devbase/tui/dispatch.py` -- `tests/env/test_runtime_store.py`(新設)、`tests/cli/test_up_roundtrips.py`(新設)、 - `tests/cli/tui/test_dispatch.py`(足す)、`tests/conftest.py`(autouse で `release_store()`) - -### Task 1: `runtime.store_for` / `release_store` と、`resolve` / `inject` / `child_env` の切り替え - -- **対象ファイル:** `lib/devbase/env/runtime.py`、`tests/env/test_runtime_store.py` -- **変更内容:** モジュールの控え(`_store` / `_store_root`)と 2 関数を足す。`store` 引数が - `None` のとき `store_for(root)` を使う。明示的に渡された `store` は控えに入れない -- **満たす受け入れ条件:** `store_for` の規則の表(設計) -- **進め方:** 同一性・`root` 変更・解放後の作り直し・明示 `store` を控えない、の 4 テストを先に書く - -### Task 2: `_ensure_env_files` を持ち回った store に切り替え、`env init` の後に捨てる - -- **対象ファイル:** `lib/devbase/commands/container.py`、`tests/cli/test_up_roundtrips.py` -- **変更内容:** `SecretStore(devbase_root)` → `runtime.store_for(devbase_root)`。`env init` の - 子プロセスから戻ったら終了コードによらず `runtime.release_store()`(決定 5) -- **満たす受け入れ条件:** 4(GET が増えない)、8(`env init` が書いた値で起動する) -- **進め方:** 偽サーバで注入 → `_ensure_env_files()` → GET 件数不変のテスト、`subprocess.run` を - 偽サーバへ書くスタブに差し替えて `_run_deploy_pipeline` へ渡る `SecretEnv` を見るテスト - -### Task 3: `_dispatch_lifecycle` の `finally` で捨てる(3 経路の往復を固定) - -- **対象ファイル:** `lib/devbase/commands/container.py`、`tests/cli/test_up_roundtrips.py` -- **変更内容:** `finally` に `runtime.release_store()` を並べる -- **満たす受け入れ条件:** 1・2・3(認証 1 回 + GET 4 / ≤6 / 4)、5・6(既存テスト無変更) -- **進め方:** `cli._load_secret_env` → `container.cmd_project(ns)` を偽サーバ + docker 差し替えで - 走らせ、`openbao.logins` と GET の `kv_path` の集合を固定する - -### Task 4: TUI の委譲の入口で捨てる - -- **対象ファイル:** `lib/devbase/tui/dispatch.py`、`tests/cli/tui/test_dispatch.py` -- **変更内容:** `_preserve_cwd_env` の入口で `runtime.release_store()`(決定 3) -- **満たす受け入れ条件:** 9、決定 3 の規則 -- **進め方:** `store_for(root)` で控えを作ってから `dispatch_group` の handler 内で別インスタンスに - なるテスト、`env edit`(エディタのスタブ)→ `up` で新しい値が渡るテスト - -### Task 5: 既存テストの独立性 - -- **対象ファイル:** `tests/conftest.py` -- **変更内容:** autouse fixture で各テストの前後に `runtime.release_store()`。モジュールの控えが - テストをまたいで残らない -- **満たす受け入れ条件:** 5・7 -- **進め方:** `uv run pytest tests/` 全件 - -### リスクと対処 - -| リスク | 対処 | -| --- | --- | -| `tests/cli/` の既存 harness が `SecretStore(root)` を直接作り `monkeypatch` している | Task 5 の autouse fixture。実装時に `grep -rn "SecretStore(" tests/` で数える | -| `container.py` は 1300 行超で、`_ensure_env_files` と `_dispatch_lifecycle` が離れている | 触るのは 2 関数の数行。タスクごとにテストを通す | - -### 切り戻し手順 - -- 差分を戻すだけ(永続データ・スキーマの変更なし)。`release_store()` を呼ばない古い経路が - 残っても、`SecretStore` を作り直す従来の動きに戻るだけで壊れない - -### 完了の定義 - -- [ ] 受け入れ条件 1〜9 をすべて満たし、条件ごとに検証手段と結果が対応している -- [ ] `uv run pytest tests/` / `ruff check lib` / `python -m compileall -q lib bin` が exit=0 From 0d3f32461d27239cb320886e8e8021a7ae894977 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 15 Sep 2026 16:16:02 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20env=20init=20=E3=82=92=E8=B5=B0?= =?UTF-8?q?=E3=82=89=E3=81=9B=E3=81=9F=20up=20=E3=81=AE=E5=BE=80=E5=BE=A9?= =?UTF-8?q?=E3=81=8C=20up=20=E3=81=AE=E3=83=97=E3=83=AD=E3=82=BB=E3=82=B9?= =?UTF-8?q?=E5=88=86=E3=81=A0=E3=81=91=E3=81=A7=E3=81=82=E3=82=8B=E3=81=93?= =?UTF-8?q?=E3=81=A8=E3=82=92=E6=98=8E=E8=A8=98=E3=81=99=E3=82=8B?= 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_01WWoEdi3vSQQnLLas1fVNLL --- docs/specifications/secret-backend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specifications/secret-backend.md b/docs/specifications/secret-backend.md index d17933fc..93ec32ae 100644 --- a/docs/specifications/secret-backend.md +++ b/docs/specifications/secret-backend.md @@ -280,7 +280,7 @@ flowchart LR | `devbase up`(プロジェクト `web` の中) | 1 | 4 | | `devbase up web`(別のプロジェクト `api` の中) | 1 | 6(`api` の 4 + 切替後の `web` 固有の 2) | | `devbase up web`(`projects/` の外) | 1 | 4 | -| 共通機密が未作成で `env init` を走らせた `up` | 2 | 8 以下 | +| 共通機密が未作成で `env init` を走らせた `up`(`up` のプロセスの分だけ。子プロセスの `env init` の往復は含まない) | 2 | 8 以下 | ### コンテナの中の `bao`