feat: base イメージに bao を入れ、起動中のコンテナから機密を読み書きできるようにする (PLAN54) - #178
Conversation
- containers/base: OpenBao CLI 2.6.2 を checksums.txt で検証して導入 - up / scale: backend が openbao のとき BAO_ADDR を渡し、~/.vault-token へ token を書く - devbase env token [--print] [--context NAME]: 起動中の dev コンテナの token を取り直す Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWoEdi3vSQQnLLas1fVNLL
Add characterization tests for docker ps failures, continued token distribution after subprocess exceptions, and recovery from malformed OpenBao login responses. Item-Id: R1-001 Round: 1 Impl-Runtime: codex Impl-Model: default
改修計画 — devbasex/devbase #178
ラウンド 1(実装 codex / レビュー agy / kiro)R1-001 —
|
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | codex / kiro | 採用 | 1 |
なぜ: cmd_env_token は _running_dev_containers 経由で docker ps の失敗を 2 経路 (subprocess 例外 / 非ゼロ終了) 拾い、None を受けて 1 を返しログインしないが、既存テストは成功系・空出力 (起動中コンテナなし)・exec 部分失敗だけを固定しており、docker ps 自体の失敗は固定されていない
手順: 1. プロジェクトと openbao backend の公開インタフェースを持つスタブを用意し、認証は実サーバへ接続しない。
2. runner の docker ps 応答を非ゼロ終了、FileNotFoundError、TimeoutExpired にパラメータ化し、cmd_env_token を呼ぶ。
3. 現実装の終了値 1 と空の標準出力を採取して固定する。発行済み token のスタブ状態が空で、runner に docker exec 要求が届かないことも確認する。private な列挙関数は直接呼ばない。
R1-002 — lib/devbase/commands/env.py#cmd_env_token
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | codex | 採用 | 1 |
なぜ: 既存の選別テストは db・snapshot・devtools を除外するが、番号の数値順、0・先頭ゼロ・非数値の番号、タブのない行を扱う経路は固定していない。
手順: 1. 認証 backend をスタブ化し、runner の ps 出力に dev-10、dev-2、dev-1 をこの順で入れ、dev-0、dev-01、dev-x、タブのない行を混ぜる。
2. cmd_env_token を実行し、docker exec の宛先と標準出力のコンテナ名を採取する。
3. 現実装で選ばれた 1・2・10 の順序と終了値 0 を固定し、不正な行の名前が配布先に入らないことを確認する。表示全文や内部関数には結合させない。
R1-003 — lib/devbase/env/container_token.py#push
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | codex | 採用 | 1 |
なぜ: 既存テストは非ゼロ終了後の継続と FileNotFoundError 単独を固定しているが、SubprocessError 系の例外が配布途中で起きても後続へ配布する経路は固定していない。
手順: 1. 3 コンテナを渡し、中央だけ TimeoutExpired(output・stderr にダミー token を含める)を送出する runner を用意する。CalledProcessError もパラメータ化する。
2. 公開入口 push を実行し、戻り値と runner が受け取った配布先・stdin、警告を採取する。
3. 現実装で観測した成功先が先頭と末尾であること、後続へ token が届くこと、例外内の token がログへ出ないことを固定する。内部呼び出し回数や警告全文は比較しない。
R1-004 — lib/devbase/env/openbao.py#OpenBaoBackend.issue_token
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | codex | 採用 | 1 |
なぜ: 既存テストは正常な認証応答と HTTP 400/403 を固定しているが、HTTP 成功でも auth/client_token が欠落・不正型・空である認証応答を固定していない。偽サーバは常に正しい auth を返す。
手順: 1. 設定・資格情報をテスト内に用意し、urllib.request.urlopen を HTTP 200 の JSON 本文を返す通信スタブに置き換える。本文をトップレベル配列、auth 欠落/null、client_token 欠落/数値/空文字にパラメータ化する。
2. 新しい backend の公開入口 issue_token を実行し、現実装の例外型 SecretUnreachableError を採取して期待値にする。
3. 応答を有効なダミー token に切り替え、同じ backend の issue_token がその token を返すことを固定する。不正応答で利用不能な token を保持しないことを公開結果で確かめ、private な状態やメソッドは検証しない。
ラウンド 2(実装 agy / レビュー codex / kiro)
R2-001 — lib/devbase/commands/container.py#cmd_scale
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| duplication | consolidate_duplication | major | kiro | 採用 | 1 |
なぜ: cmd_scale と _run_deploy_pipeline が同じ 3 手 (_inject_secrets(required=True) → dev_environment = {**container_env, **_bao_environment()} → _generate_compose_for(..., **_remote_generate_kwargs(target))) を持つ。スケール構成の作り方という同じ業務規則で、bao 環境や remote 引数の追加時に必ず両方を直す必要がある。差異は scale 値と _remote_generate_kwargs の target 参照だけ。
手順: 1. _build_scaled_override(scale, config, project_name, target) を container.py に抽出し、_inject_secrets(required=True)・dev_environment 合成・_generate_compose_for(scale, secrets, dev_environment=..., **_remote_generate_kwargs(target)) をまとめて override_file を返す
2. _run_deploy_pipeline 内の [2/6] ブロックの当該 3 行を新関数呼び出しへ置換 (down の前後関係は _previous_scale_compose のブロック内のまま維持)
3. cmd_scale の [3/6] ブロックも同関数呼び出しへ置換
4. tests/commands/test_container_up_order.py と tests/commands/test_container_bao.py を実行し順序と BAO_ADDR 合成が不変であることを確認
R2-002 — lib/devbase/commands/container.py#_resolve_dev_service
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| duplication | consolidate_duplication | minor | kiro | 取り消し | 1 |
なぜ: _resolve_dev_service と _read_compose_services がどちらも docker compose config --format json を subprocess.run し、returncode を確認して json.loads する同じ取得手順を持つ。compose config の読み方という同じ関心で、コマンドや解析方法を変えるときは両方を直す。_resolve_dev_service は失敗時 None、_read_compose_services は (returncode, services) を返す点だけが異なる。
手順: 1. _read_compose_services を唯一の compose config 読み取りにする
2. _resolve_dev_service を _read_compose_services を呼ぶ形に書き換え、returncode!=0 または JSONDecodeError のとき None を返す既存の振る舞いを保つ (services.get(get_dev_service_name(), {}) を返す)
3. json.loads の例外飲み込みの差 (_read_compose_services は raise、_resolve_dev_service は None) を維持するため、_resolve_dev_service 側で try/except を残す
4. tests/cli/test_rebuild.py と tests/cli/test_base_image_staleness.py を実行し不変を確認
R2-003 — lib/devbase/commands/container.py#_base_image_is_fresh
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| duplication | consolidate_duplication | minor | kiro | 採用 | 1 |
なぜ: docker image inspect <ref> を実行し returncode を確認して _get_image_age_days(inspect.stdout) で日数へ変換する手順が _base_image_is_fresh・_build_resolved・_ensure_images の 3 箇所にほぼ同形で現れる。イメージの作成日を取る方法という同じ関心で、inspect の呼び方を変えると複数箇所を直す。
手順: 1. _inspect_image_age(ref) -> Optional[int] を抽出し、subprocess.run(['docker','image','inspect',ref]) → returncode!=0 なら None → _get_image_age_days(stdout) を返す
2. _base_image_is_fresh の inspect/returncode/age_days 取得を _inspect_image_age(base_ref) 呼び出しへ置換 (以降の閾値判定ログはそのまま)
3. _build_resolved・_ensure_images の inspect 呼び出しは、返り値 (returncode) で分岐して build/pull を選ぶため stdout も必要な箇所のみ据え置き、age 判定に使う経路だけ新関数へ寄せる
4. tests/cli/test_base_image_staleness.py・tests/cli/test_rebuild.py を実行し不変を確認
R2-004 — lib/devbase/env/openbao.py#OpenBaoBackend.save
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| duplication | consolidate_duplication | minor | kiro | 採用 | 1 |
なぜ: fetch・save・remove の 3 経路が HTTP 403 に対し SecretAuthError を組み立てる際、({ref.label()}: HTTP 403) と 接続先: {self.url}・パス: {self.display_path(ref)} という同じ封筒を繰り返している。既存の _unreachable / _unreadable と同じヘルパ idiom で 403 だけ共通化されていない。操作固有の文言 (読む/書き込み/削除権限・openbao.user 等) は test で固定されているため封筒のみ共通化する。
手順: 1. _forbidden(self, ref, action, *, hint='') -> SecretAuthError を追加し、共通の封筒 (label・HTTP 403・接続先・パス) を組み、action と hint で操作固有の文言を差し込む
2. fetch の 403 分岐を _forbidden(ref, '読む', hint=backend.yml.openbao.user 案内) へ置換
3. save の 403 分岐を forbidden(ref, '書き込み', hint=チーム置き場案内) へ置換
4. remove の 403 分岐を forbidden(ref, '削除') へ置換
5. tests/env/test_openbao.py の test_get_403/test_write_403 ほか 403 系を実行し、読む権限/書き込み権限/openbao.user の文言が保たれることを確認
ラウンド 3(実装 kiro / レビュー codex / agy)
R3-001 — lib/devbase/commands/env.py#cmd_env_project
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| long_method | extract_method | major | codex / kiro | 採用 | 1 |
なぜ: プロジェクト参照の解決・YAML読み込み・変数ごとの既存値判定/乱数生成/必須入力判定・手入力ループ・保存を1関数が担当している。特にYAMLの変数処理がファイル有無の分岐内に入り、必須値未入力時の保存中止を対話処理と同時に追う必要がある。tests/env、tests/commands、tests/containers、tests/cliを cmd_env_project、env_project、env.yml、env project 等で検索したが、この対話処理を実行するテストは確認できなかった。TUI側のprojectテストはdispatchを差し替えており、引数解析の--user拒否テストも関数本体を通らない。
手順: 1. tests/commands/test_env_project.py に公開入口 cmd_env_project を通す現状固定テストを先に書く。一時ディレクトリの実 SecretStore を使い、safe_input と secrets を境界で固定する。固定する経路: env.yml の variables 処理 (既存値スキップ・generate 既定長・generate 明示長・任意値の空入力・必須値の空入力による中止=戻り値1)、env.yml 不在時の手入力ループ (KEY=VALUE / 不正形式 / EOF 終了)、保存後の件数表示。
2. env.yml の variables を処理するループを同ファイルの _collect_from_env_yml(env_file, variables) -> bool へ抽出する。判定順序 (existing → generate → required) と生成方法をそのまま移し、必須未入力のときだけ False を返す。呼び出し側は False で save せず 1 を返す。
3. env.yml 不在時の案内表示と手入力ループを _collect_interactively(env_file) へ抽出する (EOFError の捕捉範囲も一緒に移す)。
4. cmd_env_project には参照解決・env.yml 有無の選択・共通の save と完了ログだけを残す。
5. 手順1の固定テストと uv run pytest -q tests/ を実行し、表示・保存タイミング・戻り値が不変であることを確認する。見積 190 行 = 抽出本体の移動 (追加+削除) 約60行、呼び出し側の書き換え約10行、先に足す現状固定テストと fixture 約120行。
R3-002 — lib/devbase/env/openbao.py#OpenBaoBackend
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| duplication | consolidate_duplication | minor | kiro | 採用 | 1 |
なぜ: OpenBao の応答 (KV v2 の封筒) を辿る x.get(k) if isinstance(x, dict) else None の防御的な取り出しが _HttpStatus.errors・login・_version_of・_parse_secrets・save の応答解釈に 10 箇所ほど散る。すべて同じ封筒 (data / data.data / data.metadata / auth / errors) を辿るためで、封筒の形が変われば一緒に変わる (偶然の一致ではない)。同じ判断が並ぶと 1 箇所だけ isinstance を落とす取り違えが起きうる。
手順: 1. モジュールに _dict_get(value, key) -> Any (value が dict なら value.get(key)、そうでなければ None) を追加する。
2. login の auth/client_token 取り出し、_version_of の data/metadata/version、_parse_secrets の data/data、save の応答 data/version、_HttpStatus.errors の errors 取り出しを _dict_get の連なりへ置き換える (型・空判定の後続チェックはそのまま残す)。
3. tests/env/test_openbao.py を実行し、404・版欠落・値が文字列でない・CAS 不一致・保存応答の version 欠落などの解釈が不変であることを確認する。見積 32 行 = ヘルパ追加約5行、10 箇所の置換 (追加+削除) 約27行。既存テストで守られているため test_gap は偽。
R3-003 — lib/devbase/commands/env.py#cmd_env_token
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| long_method | extract_method | minor | kiro | 検証中 | 1 |
なぜ: 1 関数が backend 判定・--print 分岐・プロジェクト解決・プロジェクト env 読み込み・dev サービス名解決・docker context 反映・起動中コンテナ列挙・ログイン・書き込みまでを通しで行う。try: ... except DevbaseError: logger.error; return 1 が backend 名取得・issue_token(print)・issue_token(配布) の 3 箇所で繰り返され、段階の切れ目が読みにくい。
手順: 1. backend が openbao であることの確認 (store 取得→backend_name→非 openbao で error/return 1) を _require_openbao_backend(store) -> Optional[backend] へ抽出する。
2. 「起動中の dev コンテナへ配る」本体 (プロジェクト解決→env 読み込み→context 反映→_running_dev_containers→login→push→失敗集計) を _push_token_to_running(devbase_root, backend, context, run) -> int へ抽出する。cmd_env_token は backend 判定と print/配布の 2 分岐だけを持つ形にする。
3. issue_token を DevbaseError で包んで 1 を返す小片が print/配布で重複するので、その 1 手だけ _issue_or_error(backend) にまとめる。
4. tests/commands/test_env_token.py の全ケース (print・projects 外・非 openbao・部分失敗・context 適用・docker ps 失敗) と uv run pytest -q tests/ を実行し、docker 呼び出し順序・戻り値・ログイン回数が不変であることを確認する。
見送った項目
| ラウンド | 対象 | 兆候・経路 | 理由 |
|---|---|---|---|
| 2 | lib/devbase/commands/container.py#_resolve_dev_service |
duplication | コミット f23ca4f にトレーラーが欠けています: Item-Id, Round, Impl-Runtime, Impl-Model |
Add characterization test for numeric ordering and filtering of dev container names in cmd_env_token. Item-Id: R1-002 Round: 1 Impl-Runtime: agy Impl-Model: default
cmd_scale と _run_deploy_pipeline が持つ「機密復号 → dev 環境 (BAO_ADDR 含む) 合成 → 構成生成」の同じ 3 手を _build_scaled_override へ抽出して 1 箇所へ寄せた。 OpenBaoBackend の fetch/save/remove が繰り返す HTTP 403 の封筒 (label・HTTP 403・ 接続先・パス) を _forbidden ヘルパへまとめ、操作固有の文言は action / hint で差し込む。 remove はパス行を持たないため show_path=False で従来出力を維持。振る舞いは不変。 Item-Id: R2-001 Round: 2 Impl-Runtime: kiro Impl-Model: default
…#_resolve_dev_service _resolve_dev_service が持っていた `docker compose config --format json` の 取得と解析を _read_compose_services へ寄せる。終了コードが 0 以外、または JSON が壊れているときに None を返す振る舞いは維持する。 Item-Id: R2-002 Round: 2 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ainer.py#_resolve_dev_service" This reverts commit f23ca4f.
…#_base_image_is_fresh Extract image age inspection while preserving freshness thresholds and logging. Keep project image inspection in place because its status and stdout drive build and pull decisions. Item-Id: R2-003 Round: 2 Impl-Runtime: codex Impl-Model: default
- cmd_env_project から _collect_from_env_yml と _collect_interactively を抽出 (R3-001) - tests/commands/test_env_project.py に現状固定テストを追加 (R3-001) - OpenBaoBackend の辞書安全取得を _dict_get へ集約 (R3-002) Item-Id: R3-001 Round: 3 Impl-Runtime: agy Impl-Model: default
cmd_env_token が backend 判定・--print 分岐・プロジェクト解決・env 読み込み・ context 反映・起動中コンテナ列挙・ログイン・書き込みを通しで行っていたため、 段階ごとにヘルパへ抽出して分岐だけを残す形にした。 - _require_openbao_backend(store): store 取得→backend_name→非 openbao の error/return をまとめ、対象 backend か None を返す - _push_token_to_running(devbase_root, backend, context, run, runner): 配布本体 (プロジェクト解決→env→context→列挙→login→push→失敗集計) を抽出 - _issue_or_error(backend): issue_token を DevbaseError で包む小片を共通化し、 print/配布の重複を除いた docker 呼び出し順序・戻り値・ログイン回数・container_token.push への runner の 受け渡しは不変。振る舞いは変えていない。 Item-Id: R3-003 Round: 3 Impl-Runtime: kiro Impl-Model: default
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | kiro | REQUEST_CHANGES
bao バイナリのサプライチェーン検証に 1 点、すり抜け経路がある(インライン参照)。他は設計・分割とも妥当。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | agy | APPROVE
PLAN54 の要件およびリファクタリング計画に沿って実装・テストされており、指摘事項はありません。
grep | sha256sum -c - のパイプでは grep の終了値が捨てられ、検証が sha256sum の 空入力の挙動に依存していた。grep の結果を先に変数へ取り出し、set -e の下で ヒットしなければそこで止める。形を固定するテストを足す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWoEdi3vSQQnLLas1fVNLL
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | APPROVE
新規の修正指摘はありません。関連テスト 161 件通過(実 Docker・実 OpenBao サーバでの検証は未実施)。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | kiro | APPROVE
PLAN54 の bao token 配布と R2/R3 のリファクタリングを確認した。新機能・抽出とも設計は妥当で、修正を要する指摘は無い。関連テスト (test_container_bao / test_env_token / test_container_token / test_openbao / test_env_project / test_container_up_order = 92 件) はローカルで全て green。
確認した観点: token を stdin のみで渡し argv/環境変数/ログに残さない点、mktemp→chmod 0600→mv -f の原子的置換、up/scale の _build_scaled_override 共通化と順序不変、_inspect_image_age / _forbidden / _dict_get の抽出が既存の戻り値・例外・文言を保っている点、store_for 共有によりログインが増えない点。いずれも既存挙動と一致しており、追加の修正提案は無い。
概要
起動中の dev コンテナの中で OpenBao の CLI
baoを使い、再起動せずに自分の機密を読み書きできるようにする。要求と設計は
issues/PLAN54_bao-in-container.md/issues/PLAN54_bao-in-container-design.md(設計 PR #175 でマージ済み)。関連 Issue
変更点
containers/base/Dockerfile:ARG BAO_VERSION=2.6.2。amd64 / arm64 の tar.gz を同じリリースのchecksums.txtで検証してからbaoだけを/usr/local/binへ。対象行の取り出しは独立の命令で、0 件ならビルドを止める(決定 5・6)env/openbao.py:issue_token()(注入と同じSecretStoreならログインを増やさない)env/container_token.py(新設):docker exec -iの stdin で token を渡し、mktemp→chmod 0600→mv -fで~/.vault-tokenを置き換えるcommands/container.py: backend がopenbaoのとき dev サービスへBAO_ADDRを足し、upの [5/6] の後に各 dev コンテナへ token を書く(失敗は警告、upは倒さない。決定 3・4)。token は PLAN55 のruntime.store_forから取るcmd_scaleにも同じ 2 つを入れた(設計はupだけを挙げていた。増やしたインスタンスにBAO_ADDRと token が無い状態を作らないため。計画に記録)devbase env token [--print] [--context NAME](commands/env.py/cli.py): 対象のコンテナが見つかってからログインする。プロジェクト直下のenvから dev サービス名、project.local.ymlから接続先を決める(決定 2)docs/user/env-backend.md「コンテナの中からbaoを使う」、CLI リファレンスにenv tokencross-refactoring、3 ラウンド): token まわりの失敗系の現状固定テスト 4 件、cmd_scaleの構成生成・OpenBao の 403 応答・_base_image_is_freshの重複の統合、cmd_env_project/cmd_env_tokenの extract method。見送り 1 件(_resolve_dev_service、ai-plugins#553 のトレーラー欠落による取り消し)検証結果
head
7a09e1b。合否は終了コードで判定。uv run pytest -q tests/containers/test_base_dockerfile_bao.py tests/env/test_container_token.py tests/env/test_openbao.py tests/commands/test_container_bao.py tests/commands/test_env_token.py tests/cli/test_up_roundtrips.py tests/cli/test_prefix_resolution.pyuv run pytest -q tests/uvx ruff check --select=E9,F63,F7,F82 lib(CI と同じ選択)libpython3 -m compileall -q lib binlibbinubuntu:26.04+ curl の最小イメージでdocker build(arm64)bao version→OpenBao v2.6.2。checksums の値を改ざん → exit=1、対象行 0 件 → exit=1WRITE_COMMANDをubuntu:26.04で実行(既存 0644 の~/.vault-token)cross-review2 ラウンドカバレッジツールの設定は無い(
pyproject.tomlに[tool.coverage]なし)ため閾値の判定は行わない。受け入れ条件(PLAN54): 10 件のうち 6 件を満たし、4 件はリリース後テストで確かめる。
bao versionがOpenBao v2.6.2→ arm64 の最小イメージで確認(base イメージ全体のビルドと amd64 は未検証。下記)test_base_dockerfile_bao.pybao kv getがホストのenv get --userと同じ → リリース後テストkv patchがホストに見える → リリース後テストteam/globalは読めて書けない(403) → リリース後テストdevbase env tokenで読める → リリース後テスト(振る舞いはtest_env_token.pyで固定)ageならBAO_ADDR/ token が無い →test_container_bao.py::test_age_up_adds_nothing_and_does_not_execdocker inspectに出ない →test_container_token.py、test_container_bao.py::test_bao_addr_is_the_only_bao_value_in_the_environmentpytest/ruff/shellcheck→ 上の表(shellcheck は CI)tests/containers/test_base_dockerfile_bao.py(5 件)未検証の項目: base イメージ全体の
devbase build(amd64 / arm64)と受け入れ条件 3〜6 — PLAN53 の切り替え後のリリース後テストで行う既存の失敗: なし
範囲外と判断したもの: cross-refactoring で claude 担当のコミットがトレーラー欠落で取り消される件 → 既存の devbasex/ai-plugins#553 へ由来を追記
🤖 Generated with Claude Code
https://claude.ai/code/session_01WWoEdi3vSQQnLLas1fVNLL