release: PLAN39 永続化ボリュームをアカウントグループ単位に分離する - #122
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | REQUEST_CHANGES
切り戻し時のデータ保全手順と、グループ名の検証方針を計画内で整合させてください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | REQUEST_CHANGES
PLAN39 の設計書をレビューしました。全体的なボリューム分離とフォールバックの設計は妥当ですが、entrypoint.sh の既存の実行順序と衝突してサービスアカウント鍵が欠落するロジックの不備(Task 5)と、既存状態の前提誤り(AC8)があるため、実装に入る前にドキュメントの修正を提案します。
※ インラインコメントにて具体的な修正箇所を指摘しています。
- 前提 8 を追加し、GCP credentials 生成が symlink 生成より先である現行順序を記録する - Task 4 に symlink ブロックを credentials 生成より前へ移動する変更を追加する(AC11 新設) - AC8 から gcloud を外し、.config/gcloud / .config/gws はシード元が無く全グループで初回 1 回の 再認証が要ることを明記する(互換性表・代替案 A' も同様に修正) - Task 1 に数字のみのグループ名の拒否を追加し、リスク表の記述と一致させる - Task 5 の補足を実装に合わせ、プロファイル切替では上書きされること・鍵未設定プロファイルで スキップされたときだけ旧鍵が残ることに書き直す - 切り戻し手順に、revert 前の同期・検証・競合時の扱いを追加する Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
🔧 /ndf:fix サマリ (round 1)対応件数: critical=1 / major=2 / minor=2 (合計 5 件、全件修正) 変更は 詳細
決定事項の維持
5 スレッドすべて reply 済み・Resolve 済みです。再レビューをお願いします。 |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | REQUEST_CHANGES
PR 説明は default の初回シードにより再ログイン不要と断定し、結合テストにも既存プロジェクトでの再ログイン不要を掲げていますが、更新後の AC8 は gcloud / gws について全グループで初回再ログインが必要としています。実際の移行時の期待を誤らせるため、PR 説明と Test plan を AC8 に合わせて更新してください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | gemini | REQUEST_CHANGES
プランの修正により、永続化のスコープや旧ファイルの挙動に関する疑問点が明確化されており、大変良い改善です。切り戻し手順におけるファイル存在確認の考慮漏れが運用上のリスクとなるため、1 点のみ修正を依頼します。
- `devbase down` 後はコンテナが消えるため `docker cp` が使えない。`.config/gcloud` / `.config/gws` の退避を、グループボリュームを一時コンテナへマウントして tar で ホストへ書き出す形へ置き換える - 分類 B の書き戻しを `&&` 連結から存在確認付きのループへ変更し、`/from` 側に `.claude.json` / `.claude` / `.gemini` が未作成でも以降の同期が止まらないようにする - 記載したコマンドは実ボリュームに対して実行し、欠落時 skip・tar 生成の双方を確認済み Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
🔧 /ndf:fix サマリ (round 2)対応件数: critical=0 / major=2 / minor=0 (合計 2 件) 未 Resolve だった 2 スレッドはいずれも round 1 で書き換えた「切り戻し手順」への指摘で、両方とも妥当だったため修正しました。 詳細
補足:
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | codex | REQUEST_CHANGES
認証プロファイル切替時に、以前のサービスアカウント鍵が残存・再利用されない設計へ修正が必要です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | gemini | APPROVE
切り戻し手順のコマンド修正について、意図・実装ともに妥当であることを確認しました。
devbase down でコンテナが消滅する仕様に合わせたボリュームの直接マウントや、存在確認による cp のフェイルセーフ化、POSIX 互換の安全な tar 退避など、切り戻し時の運用リスクが適切に軽減されています。追加の修正提案はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | gemini | APPROVE
切り戻し手順のコマンド修正について、意図・実装ともに妥当であることを確認しました。
devbase down でコンテナが消滅する仕様に合わせたボリュームの直接マウントや、存在確認による cp のフェイルセーフ化、POSIX 互換の安全な tar 退避など、切り戻し時の運用リスクが適切に軽減されています。追加の修正提案はありません。
GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE は鍵の有無に関係なく /home/ubuntu/.config/gcloud/credentials.json 固定で env に書かれる (lib/devbase/env/collectors/google.py:139-141)。一方 entrypoint の生成ブロックは 全体が `if [ -n "$_GCP_CREDS_B64" ]` (containers/base/entrypoint.sh:196) の内側で、 鍵が無いときに古い実体を消す経路が無い。Task 5 で .config/gcloud を永続化すると、 鍵が未設定のプロファイルへ切り替えたときに旧プロファイルの鍵が残り、 別顧客のサービスアカウントを ADC 経由で使えてしまう。 - 前提 9 として上記の 2 つの実装事実を追加 - Task 5 の補足を「ドキュメント化する」から「鍵なし時は管理対象パスを rm -f し 両変数を unset する」仕様へ変更(gcloud のユーザー OAuth は別ファイルなので影響なし) - AC12 としてプロファイル切替テストを追加し、Task 5 / 完了の定義へ反映 - リスク表の該当行を新しい対処へ更新 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
🔧 /ndf:fix サマリ (round 3)対応件数: critical=0 / major=1 / minor=0 (合計 1 件) round 3 は gemini が APPROVE(指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。妥当な指摘だったため修正し、Resolve 済みです。 詳細
実コードで確認した事実指摘を鵜呑みにせず、実装を読んで裏を取りました。
Task 5 で 反映内容 (
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | codex | REQUEST_CHANGES
削除対象の所有判定を明確化し、ユーザー指定ファイルを削除しない設計に修正してください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | gemini | APPROVE
ドキュメントの前提調査・修正方針ともにソースコードの現状と整合しており、提案されているフェーズ分割やリスク対処(切り戻し手順など)も妥当です。修正提案はありません。
GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE はプロジェクトの env から 任意パスへ上書きでき、entrypoint もその値を優先するため、「変数が指す先を消す」 仕様だと devbase 管理外の鍵まで削除しうる。削除先を entrypoint 内の固定パス DEFAULT_CREDS_PATH に限定し、カスタムパス指定時は削除も unset もせず WARN の みを出す仕様へ書き直した。前提 10 とリスク行を追加し、AC12 に「カスタムパスは 削除されないこと」のケースを足した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
🔧 /ndf:fix サマリ (round 4)対応件数: critical=0 / major=1 / minor=0 (合計 1 件) round 4 は gemini が 2 ラウンド連続 APPROVE(指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。round 3 の修正( 詳細
実コードで確認した事実round 3 で書いた「管理対象パスのみ
したがって、ホストからマウントした鍵を 反映内容 (
|
| 条件 | 振る舞い |
|---|---|
| 鍵あり | 現行どおり GAC_PATH / BQ_PATH へ書き chmod 600 して export(変更なし) |
鍵なし、かつ 両変数が未設定または $DEFAULT_CREDS_PATH と一致 |
rm -f "$DEFAULT_CREDS_PATH" でこの 1 パスだけ削除し、両変数を unset してログに出す |
| 鍵なし、かつ どちらかがカスタムパスを指す | 何も削除しない。unset もしない。 devbase 管理外である旨と手動削除の案内を含む WARN のみ出して起動を続行 |
カスタムパスを消さない理由(利用者の管理下でありうるため devbase が所有を主張できない)と、混線を断ちたい場合は変数を env から外して既定パスへ戻せば削除対象になる旨を追記(docs/user/container-operations.md へ記載)
所有マーカー案は不採用として理由を明記 — マーカーが永続ボリュームに残る追加状態となり、削除の安全性がマーカーの健全性に依存するため
AC12 に (5) を追加 — カスタムパス設定時に鍵未設定へ切り替えてもファイルが残ることと WARN が出ることを確認。tests/containers/ の単体テストにも「既定パス → 削除 / カスタムパス → 非削除 + WARN」の 2 ケースを固定。AC12 の見出しも「既定パスの credentials.json が残らない」+「カスタムは削除されない」へ修正
リスク表に 1 行追加し、既存行の対処も DEFAULT_CREDS_PATH 限定へ更新
決定事項の維持
.claude.json / .claude/.credentials.json はグループ別(分類 B)、.ssh は共通(分類 A)という設計判断は今回の指摘に該当がなく、変更していません。実装コード (lib/ containers/) にも触れていません(docs_only PR)。entrypoint への実装は PR3 (feature/PLAN39-gcloud) で行います。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | codex | REQUEST_CHANGES
GOOGLE_APPLICATION_CREDENTIALS と BIGQUERY_KEY_FILE の既定・カスタム混在時にも、devbase 管理対象の旧い鍵を確実に除去する条件へ修正が必要です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | gemini | APPROVE
本 PR(PLAN39)はアカウントグループ単位の永続化ボリューム分離の全体方針として、目的・制約・タスク分割・互換性が網羅的に定義されており妥当です。
特に entrypoint.sh での symlink 処理順序と入れ子パス問題、鍵未設定時の rm -f フォールバック、スナップショット互換の維持手順など、既存実装の隙間を塞ぐエッジケースが詳細に考慮されています。技術的矛盾は見当たりません。
GOOGLE_APPLICATION_CREDENTIALS と BIGQUERY_KEY_FILE を一括で判定していたため、 片方だけがカスタムパスの混在ケースで「何も削除しない」となり、既定パスを指す もう片方の古い管理対象鍵が残って AC12 の分離要件を破っていた。 変数ごとに 未設定 / 既定 / カスタム へ分類する 4 規則と、その全 9 通りを展開した 判定表へ置き換え、場合分けの漏れが原理的に出ない形にした。AC12 に混在ケース (6) を 追加し、単体テストの固定対象も判定表 9 通りへ広げた。あわせて前提 9 / 前提 10 と 重複していた散文を削り、節の分量は据え置いた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
🔧 /ndf:fix サマリ (round 5)対応件数: critical=0 / major=1 / minor=0 (合計 1 件) round 5 は gemini が APPROVE(3 ラウンド連続・指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。妥当な指摘だったため修正し、Resolve 済みです。 詳細
修正方針同じ論点で round 3〜5 と場合分けの継ぎ足しが続いていたため、個別ケースを足すのではなく判定表へ置き換えました。 規則(codex の提案どおり、変数ごとに個別評価):
両方カスタムのときだけ削除対象が無く、混線の危険が WARN のみで残ります。devbase が所有を主張できないパスは消さないという安全側の判断で、確実に断つには両変数を 併せて更新した箇所
分量散文で場合分けを重ねる形をやめ、前提 9 / 前提 10 と重複していた説明も削ったため、判定表を 9 行に増やしても Task 5 補足 2 の節は 42 → 43 行とほぼ据え置きです。プラン全体は 454 → 458 行。 変更は 再レビューをお願いします。 |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 6 | codex | APPROVE
修正必須の指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 6 | gemini | APPROVE
全体として、現状の実装や既知のバグ(前提5の入れ子パスや、前提8の削除順序など)を極めて正確に分析し、リスクを先回りして潰す妥当な設計になっており、非常に完成度が高いです。PR分割計画とタスクの依存順序も理にかなっています。
数点、実行時の堅牢性や切り戻し時の運用フォローとして微小な追記提案をインラインに残しました。
- Task 7: entrypoint.sh は set -e で動くため、未ログイン時に gcloud が非 0 を 返すと起動ごと落ちる。ログ出力側でフォールバックする方針を明記した - 切り戻し手順 5: 対象ボリュームをマウントしたコンテナが残ると volume is in use で削除に失敗するため、先に devbase down する前提を補足した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
/ndf:fix 最終スイープ 完了報告cross-review が codex / gemini 両 APPROVE で収束(6 ラウンド)した後に残っていた未 Resolve スレッド 2 件を、最終スイープとして処理しました。いずれも最終 APPROVE ラウンドで gemini が残した minor で、ループ内の fix を経由していないものです。 対応した指摘
補足
変更内容
状態
|
Web 調査で、gcloud と gws がどちらも設定ディレクトリを環境変数で差し替えられる ことを確認した。symlink で `~/.config/gcloud` を永続化する当初案をやめ、 CLOUDSDK_CONFIG / GOOGLE_WORKSPACE_CLI_CONFIG_DIR をグループボリューム配下へ 向ける方式に変える。サービスアカウント鍵の出力先が永続領域の外に残るため、 鍵の持ち越しとその削除仕様(2 変数 × 3 状態の判定表)が不要になる。 あわせて GCP_AUTH_MODE を新設し、ユーザー認証 (ADC) を既定の経路にしたうえで 権限の都合で鍵が要る場面は key モードへ切り替えられるようにする。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
仕組みはグループごとに人が1回対話的に認証することを前提にしているが、その1回を どう実施するかがプランのどこにも書かれていなかった。新しいグループを足すたびに 手探りになるため、手順書 docs/user/google-auth.md をプランの成果物に含める。 あわせて gcloud のヘッドレス認証の3経路 (--launch-browser / --no-launch-browser / --no-browser) と --update-adc の挙動を前提15として記録する。要求されるものが 経路ごとに違い、この環境でどれが通るかは実機検証が必要なため。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
Task 8 に「実機で確認する」と書いていた項目のうち、対話ログインを伴わないものを 稼働中のコンテナ (carmo-ai-dev-1 / gcloud 582.0.0) で確認し、結果を前提として 記録する。実装時の再調査を減らすため。 - gcloud はこの環境で自動的にブラウザ非起動フローになる (DISPLAY 無しで ShouldLaunchBrowser が False)。VS Code のポート転送は関係しない - --update-adc は application-default login と等価ではない (quota project を書かない) - gws はベースイメージに無く、稼働中 14 コンテナのどれにも存在しない。設定の 永続化だけでは復旧しない - 空の named volume は root 所有で uid 1000 では書けない。SQLite 自体は動く - ADC の 3 状態 (鍵あり / 変数が空振り / 未ログイン) の挙動 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
containers/base/Dockerfile に gws の記述は無く、npm グローバル導入は 138 行の 1 行のみで @googleworkspace/cli を含まない。設定ディレクトリを永続化しても バイナリが無ければ復旧しないため、gemini-cli / codex と同じ行へ追加して ベースイメージに含める。AC2 は command -v gws が通ることを条件に含める。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVzbVaYsq8fgZFGLjozRt1
* feat(volume): アカウントグループのボリュームを解決・作成・マウントする (PLAN39 PR1) `DEVBASE_ACCOUNT_GROUP` を新設し、グループごとの永続化ボリューム `devbase_home_<group>` を作って `/persistent/group` へマウントする。 共通ボリューム `devbase_home_ubuntu` (`/persistent/ai`) は変更しない。 - `resolve_account_group()` — 引数 → 環境変数 → `default` の 3 段解決。 ボリューム名にできない文字列 / 予約語 `ubuntu` / 数字のみ を DevbaseError で 弾く。後ろ 2 つは正規表現を通過するため個別のチェックとして持つ - `get_group_volume()` — 検証を迂回する経路を作らないため解決器を必ず通す - `ensure_volumes()` — グループボリュームも作成する - 生成 compose — `/persistent/group` のマウント、`external: true` の宣言、 dev サービスへの `DEVBASE_ACCOUNT_GROUP` の受け渡し (entrypoint 用) - 未使用の `AI_VOLUME_PREFIX` を削除 (命名系統を 2 つ並べない) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * fix(volume): グループ名の検証を Docker 操作より前へ移す ensure_volumes() はグループボリューム名の解決 (= グループ名の検証) を 共有ボリューム devbase_home_ubuntu の確認・作成より後ろで行っていた。 このため DEVBASE_ACCOUNT_GROUP が不正なだけの入力エラーでも、 devbase_home_ubuntu が作られてから失敗し、Docker の状態が変わっていた。 解決を関数の先頭へ移し、Docker を一切触らずに弾くようにする。 併せて、失敗時に _create_volume が呼ばれないことを確認する回帰テストを追加。 Refs #116 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(container): AI 設定を共通 / アカウントグループの 2 層に分ける (PLAN39 PR2) entrypoint の symlink 機構を 2 系統にし、認証と会話履歴 (分類 B) を /persistent/group へ、契約に紐づかない共通資産 (分類 A) を /persistent/ai へ 振り分ける。あわせて入れ子パスの不具合を直す。 - `~/.claude` の既定をグループ側へ倒す。symlink である点は現行と同じで、 向き先だけを /persistent/group/.claude に変え、その配下へ共通資産 5 件 (plugins / skills / commands / CLAUDE.md / settings.json) の symlink を張る。 実機の `~/.claude` には子要素が 30 件あり、列挙方式では projects (1.1GB の 会話ログ) のような未列挙の子が黙って揮発するため (プラン 前提 20) - 入れ子パス対応 — link 側と実体側の**双方**で親ディレクトリを作る。以前は 実体側の作成が No such file or directory で落ち、壊れた symlink が残っていた - ファイル / ディレクトリの判定を拡張子 (`*.json`) から明示の一覧へ改める。 `.jsonl` がマッチせず history.jsonl がディレクトリとして作られていた - default グループの初回シード — 共通側の分類 B を**コピー**して初期化する (move ではないので切り戻し時に元が残る)。非 default では走らせない。 `.claude` のシードは分類 A の 5 件を除外する - 空の named volume は root 所有で作られるため、書けなければ chown する プラン文書の不変条件・AC6・分類表・Task 4・切り戻し手順を、この設計変更に 合わせて更新した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * fix(container): イメージ同梱の ~/.claude/settings.json を symlink 前に退避する Dockerfile が書き込む hooks 設定 (~/.claude/settings.json) は、symlink 張り替えの rm -rf で消えていた。settings.json を共通側の永続化対象に加えたことで、代わりに 空のプレースホルダが /persistent/ai へ残る形になる。張り替えの前に共通側へ コピーして、初回起動で hooks が失われないようにする。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(gcp): 設定ディレクトリをグループ単位にし GCP_AUTH_MODE を新設する (PLAN39 PR3) gcloud / gws の設定ディレクトリをグループボリューム配下へ向け、ユーザー OAuth が コンテナを作り直しても保たれ、かつグループをまたいで共有されないようにする。 あわせてサービスアカウント鍵に依存しない ADC を既定の経路にする。 - `CLOUDSDK_CONFIG=/persistent/group/gcloud` / `GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/persistent/group/gws` を**ホスト側の生成 compose で渡す**。entrypoint の export は PID 1 の子孫にしか効かず、 docker exec のシェルには届かない (実機で確認)。entrypoint は空の named volume が root 所有で作られる件に対応してディレクトリを用意するだけにする - `GCP_AUTH_MODE` (`adc` / `key` / 未設定=auto) を新設。`adc` では鍵を書かず、 GOOGLE_APPLICATION_CREDENTIALS と BIGQUERY_KEY_FILE を **compose の列挙から外す**。 値だけ残して実体が無いと ADC はユーザー認証へフォールバックせず DefaultCredentialsError で落ちるため、空文字ではなく「渡さない」を選ぶ - `devbase env init` は鍵を登録したときだけ上記 2 変数を書く (従来は無条件) - `@googleworkspace/cli` をベースイメージへ追加。gws はどのコンテナにも入っておらず、 設定だけ永続化しても復旧しないため - ドキュメント: `GCP_AUTH_MODE` の使い分けと、`~/.config/gcloud` が gcloud の 設定ディレクトリではなくなったこと、並行実行の制約 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * fix(gcp): ADC 判定をホストとコンテナで揃え直書きの鍵パスも外す (PR#125 review) cross-review round 1 の 2 件に対応する。どちらも「ホスト側は鍵モードのつもりで 2 変数を渡すのに、コンテナ側には鍵の実体が無い」という同じ壊れ方を招く。 - 元の compose.yml の `environment:` に `GOOGLE_APPLICATION_CREDENTIALS` / `BIGQUERY_KEY_FILE` が直書きされていると、機密の列挙を絞るだけでは生成 compose に残る。adc では鍵をどのサービスにも書かないので、`_drop_env_names` で全 サービスから名前ごと取り除く (map / list 両記法。空になれば environment ごと削除) - 鍵の有無の判定を entrypoint と揃える。`GCP_ACTIVE_PROFILE` (未設定なら default) のプロファイル 1 本だけを見て、無ければ後方互換の GOOGLE_APPLICATION_CREDENTIALS_BASE64 を見る。全プロファイル走査 (any) だと 別プロファイルの鍵だけでホストが key と判定してしまう - `GCP_AUTH_MODE=key` 宣言でもアクティブな鍵が無ければ adc へ倒す (警告付き)。 entrypoint が同じフォールバックを持つため、ホストだけ key だと実体の無い パスが docker exec のシェルへ残り DefaultCredentialsError になる Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
スナップショットの対象を「共通 + アカウントグループ」の 2 本にし、いま自分が どのグループにいるのかを status と起動ログで分かるようにする。 - snapshot — 対象ボリュームを固定値から 2 本へ。1 つのアーカイブにまとめるため コンテナ内では /source/ai と /source/group に並べてマウントする。メタデータへ 対象ボリューム名を記録し、`devbase snapshot list` にも表示する - 旧スナップショット (volume: devbase_home_ubuntu のみ) は共通ボリュームを ルートへ直接マウントする旧レイアウトとして**そのまま復元できる** - 対象ボリュームの構成が変わったら新しい世代を作る。旧世代の snar は別の レイアウトを記録しており、そこへ差分を積むと全ファイルが移動したものとして 扱われて差分が壊れるため。明示的に古い世代を指定された場合は理由を出して止める - 復元時のクリアはマウントポイント自身ではなく**各マウントの直下**を消す (busy) - `devbase status` の [環境] にアカウントグループとボリューム名を出す。グループ名が 不正でも例外にせず表示に留める (status は状態を見るコマンドで、設定の誤りで 一覧全体を出せなくする必要はない) - entrypoint の起動ログにグループと gcloud のアカウントを 1 行出す。未ログインや gcloud 不在で `set -e` の起動を落とさないようフォールバックする - ドキュメント — ボリューム構造の表、アカウントグループの説明、2 層の永続化、 初回シード、スナップショットの対象と世代分割、README / CHANGELOG Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | APPROVE
PLAN39の設計は既存の実装や潜在的な制約(gcloudの並行実行制限やentrypointの実行順序など)を正確に分析しており、非常に網羅的で妥当な計画になっています。事前のレビューラウンドで指摘されたエッジケースや切り戻し手順の安全性についてもすべて解決されていることを確認しました。追加の修正提案はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | APPROVE
修正必須の指摘はありません。
* docs(PLAN39): Google 認証の手順書を追加する (PLAN39 PR5) アカウントグループごとの gcloud / gws 認証手順を新規に書き起こす。コマンドと出力は すべて実機 (carmo-ai / gcloud 582.0.0 / gws 0.22.5) で実行した結果を貼っている。 - 前提 — アカウントグループとボリュームの対応、`~/.config/gcloud` は gcloud の 設定ディレクトリ**ではない**こと ($CLOUDSDK_CONFIG を見る) - 新しいグループの初回セットアップ — env への記述から起動・確認まで。使えない グループ名 3 種の実際のエラー出力 - gcloud — `gcloud auth login` (フラグ不要で URL + 認証コードのフローになる) と `gcloud auth application-default login` の 2 回。`--update-adc` を既定にしない理由。 コンテナを作り直しても認証が残ることの確認手順 - グループごとに別アカウントになっていることの確認 (ボリュームを直接覗く) - gws — setup が GCP プロジェクトを要すること、OAuth クライアントの手動作成、 クライアントシークレットを env に書いてはいけないこと、login が localhost コールバック方式でホストのブラウザからは中継が要ること、 制限付きスコープで同意画面が進まないときの回避 - 認証モードの切り替え — adc / key / 未設定の使い分けと鍵が要る場面 - 確認コマンド — 自分のグループの確認方法を含む - トラブルシュート — DefaultCredentialsError の 2 種類の見分け、database is locked、 意図しないアカウントで操作していた場合 README の目次にも追加した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): quota project の書き込み条件を実行結果に合わせる `gcloud auth application-default login` が quota project を必ず書くと 読める記述を、利用可能な project を解決できた場合に限る旨へ改める。 掲載している実行例は `Cannot find a quota project` となっており、 断定のままだと実出力と矛盾していた。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): 実機検証の結果を受け入れ条件へ反映する `devbase build --no-cache` 後の実機 (carmo-ai を default / kkg の 2 グループで起動) で AC1〜AC14 をすべて確認し、根拠つきの結果表を追加した。 検証中に見つかった 2 件は PLAN39 の退行ではないため別件として記録した。 - 旧世代スナップショットの incr-002 適用で GNU tar が rename に失敗する (現行 main と同じ旧コマンド形式で再現するため PLAN39 由来ではない) - $DEVBASE_ROOT/env が .gitignore の対象外で、機密を誤って書くと混入しうる Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): gws --version の実出力を 2 行とも載せる 2 行目の "This is not an officially supported Google product." が抜けていた。 実出力をそのまま貼る方針に合わせる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): 検証結果の矛盾と手順書の抜けを直す cross-review round 2 の指摘へ対応した。 - AC9 の根拠を「full + incr-001 まで」に限定し、incr-002 の失敗が PLAN39 前の main でも再現する別件であることを明示した - 完了の定義から本 PR #127 を切り出し、レビュー収束まで未チェックにした - コンテナ再作成の手順で devbase project login による入り直しを明示した (gcloud / gws の 2 箇所) - トラブルシュートに ADC の再認証を追記した。gcloud config set account と gcloud auth revoke は CLI 側しか変えないため、ライブラリ経由の呼び出しが 古いアカウントのまま残る Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): コンテナへ入り直すコマンドを devbase login <name> に直す `devbase project login` の positional はコンテナ index であり、プロジェクト名を 渡すと解決に失敗する (cli.py の _add_login_subparser / bin/devbase:357-364)。 名前で解決してくれるのはトップレベルシノニムの `devbase login <name>` なので 5 箇所を差し替えた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t * docs(PLAN39): ADC の判別方法・gws login の実行場所・key の adc フォールバックを直す - ADC の確認で `type(c).__name__` は user / service account を区別できない (両方 `Credentials`)。決め手は `__module__` である旨と実出力を追記 - 4.3 の `docker exec ... gws auth login` がホスト側であることを明示 - `GCP_AUTH_MODE=key` は鍵の env が無いと警告して adc へ倒れることを表と Warning に明記 (lib/devbase/env/gcp_auth.py の resolve_auth_mode) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | REQUEST_CHANGES
非 dev サービスの認証設定保持と、スナップショット復元メタデータの検証を修正してください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | gemini | APPROVE
PR #122 の実装全体を確認しました。PLAN39 でのボリューム分離、スナップショットの複数ボリューム対応、および CLOUDSDK_CONFIG と GCP_AUTH_MODE を用いた安全な認証切り替えが、計画書通りに矛盾なく実装されていることを確認しました。
特に懸念されていた古いサービスアカウント鍵の残留リスクについて、CLOUDSDK_CONFIG を用いて鍵自体をコンテナ層(揮発)に残す設計(代替案 E)へ変更したことで、削除時の rm -f が不要になり、カスタムパス誤削除のリスクが原理的に排除された点は非常に優れた設計判断です。
各種テスト(ボリュームマウント、Compose 環境変数の引き回し、スナップショット互換性など)も網羅されており、追加の修正提案はありません。
訂正: round 2 の指摘 1 件目について
1. 呼び出しは元から存在します( $ git show 6046a7d:containers/base/entrypoint.sh | grep -n devbase_seed_image_claude_settings
374:devbase_seed_image_claude_settings() {
399: devbase_seed_image_claude_settings "$home_root" "$ai_root"2. 実際に退避が機能することを実行して確認しました。 $ TMP=$(mktemp -d); mkdir -p "$TMP/home/.claude" "$TMP/ai"
$ echo '{"hooks":{"SessionStart":[]}}' > "$TMP/home/.claude/settings.json"
$ bash -c "DEVBASE_ENTRYPOINT_LIB_ONLY=1 . containers/base/entrypoint.sh; \
devbase_setup_ai_settings '$TMP/home' '$TMP/ai' '$TMP/group' kkg"
$ cat "$TMP/ai/.claude/settings.json" # 共通側へ退避されている
{"hooks":{"SessionStart":[]}}
$ cat "$TMP/home/.claude/settings.json" # symlink 経由で到達できる
{"hooks":{"SessionStart":[]}}3. 対応したとされたコミット $ git ls-remote origin release/PLAN39
6046a7d483e09e09358849f79a3cd545dbcd0d05 refs/heads/release/PLAN39したがって 2 件目( 🤖 Generated with Claude Code |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | codex | REQUEST_CHANGES
スナップショット復元メタデータの信頼境界を追加し、任意のホストパスを復元対象にできないよう修正してください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | gemini | APPROVE
修正提案はありません。
release PR #122 round 3 の指摘 2 件に対応する。 - `adc` での GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE の除去を **dev インスタンスだけ**に限定する。GCP_AUTH_MODE は dev コンテナの認証方式の 宣言であり、全サービスを走査すると独自に鍵をマウントしている batch のような 非 dev サービスの明示設定まで消して壊してしまう - スナップショットのメタデータ由来の対象ボリュームを検証する。`snapshot_volumes` が返す値は `docker run -v <値>:/target/<キー>` のマウント先に、キーは `clear_command` が組み立てる `bash -c` の消去コマンドに入る。meta.yml は 編集でき、スナップショットは環境をまたいで持ち込めるため、絶対パスを書けば 任意のホストディレクトリを bind mount して**復元前に中身を消せて**しまう。 キーを既知のマウント名 (`''` / `ai` / `group`) に、値を named volume として 通る名前に限定し、不正なら消去処理より前に SnapshotError で拒否する テスト: 非 dev サービスの設定が残ることを固定し、旧挙動を固定していた 2 件を 書き換えた。改変メタデータの拒否 5 件を追加した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | codex | REQUEST_CHANGES
スナップショットメタデータから復元先にできる named volume の範囲を devbase 管理対象へ限定してください。
named volume の形だけを見ていると、改変したメタデータに同じ Docker 上の無関係な
ボリューム名 (mysql_data 等) を書けてしまい、復元前の消去でその中身を失わせられる。
- 共通側 ('' / ai) は devbase_home_ubuntu に限る
- グループ側 (group) は devbase_home_<group> の形で、<group> が
resolve_account_group() を通る名前に限る (数字のみ・ubuntu も弾かれる)
無関係なボリューム名の拒否と、共通側にグループボリュームを書いた場合の拒否を
テストで固定した。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | codex | APPROVE
修正必須の指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | gemini | REQUEST_CHANGES
スナップショット復元時に指定された対象ボリュームの検証ロジック(SnapshotManager._validate_volumes)に不備があり、復元対象を不正に書き換えられるリスクが見つかりました。修正を依頼します。
round 2 の指摘 (「ADC は dev サービスの認証方式であり、非 dev サービスの明示設定は 保持する」) は直書きの除去だけでなく**機密の列挙**にも当てはまる。`adc` では GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE を secret / global / project の 列挙すべてから外していたため、元々この 2 変数を機密の env_file から受け取っていた 非 dev サービス (独自に鍵を持つ batch 等) も値を失っていた。直書きを消すのと同じく、 そのサービスだけが起動できなくなる。 - `_SecretNames` に `dev_excluded` と `for_dev` を持たせ、除外を dev インスタンスへ 渡す列挙だけに効かせる。由来別の部分集合 (`for_targets`) は絞らない - `gcp_auth.filter_key_env_names(names, mode)` を `key_only_env_names(mode)` に置き換える。 列挙を絞る側ではなく「dev から外す名前」を返す形にして、適用箇所を 1 か所にする テスト: 機密を env_file で受け取っていた非 dev サービスが adc でも GOOGLE_APPLICATION_CREDENTIALS を受け取り続けること (dev は受け取らないこと) を追加した。 ドキュメントにも除去対象が dev だけである旨を追記した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t
🔧 /ndf:fix サマリ (round 2 / 追走分)round 2 の 2 件(codex)は先行の fix ( 対応件数: major=1 (合計 1 件) / deferred: 0 件 / rejected: 0 件 詳細
品質チェック: 未 Resolve スレッド: 0 件(round 2・round 3 の指摘はすべて Resolve 済み) |
`resolve_account_group` は空文字を `default` に、前後空白を落とした名前へ **正規化する**ため、「通るかどうか」だけでは `devbase_home_` や `devbase_home_ kkg ` を弾けなかった。実際にマウントされるのは正規化前の 生の名前なので、`get_group_volume(group) == name` まで確認する。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5oA2PqY6UX2Ca3t78886t
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 6 | codex | APPROVE
修正必須の指摘はありません。
Summary
devbase は永続化ボリューム
devbase_home_ubuntu(コンテナ内/persistent/ai)を全コンテナで共有しています。一方、開発プロジェクトは複数の企業にまたがり、使用すべき Google アカウントがプロジェクトごとに異なります。この構成には 3 つの問題があります。~/.config/配下が永続化されていない —gcloud auth login/gws auth loginのユーザー OAuth はコンテナを作り直すたびに消えます。env から復元されるのはサービスアカウント鍵 (credentials.json) だけです。.claude/.credentials.jsonには各 SaaS の企業テナントに紐づく MCP OAuth トークン(Google Drive / Slack / Notion×3 / Atlassian)が入っており、Gemini もvertex-ai認証で GCP プロジェクトに紐づきます。これらを「アカウントグループ」という単位で解決します。
DEVBASE_ACCOUNT_GROUP(未設定ならdefault)を既存の 3 レベル env 機構に乗せ、永続化データを 2 つのボリュームに分けます。/persistent/aidevbase_home_ubuntu(現行のまま)~/.claude/plugins(238MB)・skills・commands・CLAUDE.md・settings.json・.codex・.serena・.kiro・.ssh・share/persistent/groupdevbase_home_<group>(新規).claude.json・~/.claude本体(認証・会話ログ・セッション状態)・.gemini・gcloud / gws の設定ディレクトリディレクトリを丸ごとグループ別にするのではなく、共通資産と認証情報を分けてそれぞれのボリュームへ symlink します。238MB の plugins をグループ数だけ複製せずに済み、既存
devbase_home_ubuntuを触らないため共通側のデータ移行も不要です。defaultグループは初回にシードするので再ログインも発生しません。実装中に変えた設計判断(3 点)
プランの記述をそのまま実装できなかった箇所です。いずれも実機で確かめた事実が理由で、プラン文書側にも前提として追記しています。
1.
~/.claudeは実ディレクトリではなく symlink のまま(PR2 / プラン 前提 20)プランは「
~/.claudeを実ディレクトリにし、配下に A / B 双方の symlink を並べる」としていましたが、実機の~/.claudeの子要素は 30 件あり、プランが名指ししていたのは 7 件だけでした。列挙方式ではprojects(1.1GB の会話ログ)・sessions・tasksのような未列挙の子がコンテナ再作成のたびに揮発します。Claude Code は版が上がるたびに子ディレクトリを増やすため、列挙漏れは今後も起きます。そこで既定を反転し、
~/.claudeは symlink のまま向き先を/persistent/group/.claudeに変え、共通資産 5 件だけをその配下から共通側へ張る形にしました。~/.claudeが symlink であること自体は現行mainと同じで、変わるのは向き先だけです。2.
CLOUDSDK_CONFIGはホスト側で渡す(PR3 / プラン 前提 21)プランは entrypoint で
export CLOUDSDK_CONFIG=...するとしていましたが、entrypoint のexport/unsetはdocker execのシェルに届きません。コンテナの環境変数はホスト側(生成 compose)が決めるもので、entrypoint が変えられるのは PID 1 の子孫だけです。GCP_AUTH_MODE=adcで 2 変数を消す条件(AC12)はdocker execで検証するものなので、この経路でしか満たせません。ホスト側(lib/devbase/env/gcp_auth.py)で解決し、compose のenvironment:の列挙から名前ごと外す形にしました。値を空文字にするのではなく渡さないのが要点です。3.
gwsをベースイメージへ含める(PR3 / プラン 前提 17)調査時点で gws はどのコンテナにも入っていませんでした(稼働中の dev コンテナ 14 本すべてで
command -v gwsが空)。設定ディレクトリだけ永続化しても復旧しないため、@googleworkspace/cliを base イメージへ追加しています。個別 PR
defaultの初回シードCLOUDSDK_CONFIG/GOOGLE_WORKSPACE_CLI_CONFIG_DIRとGCP_AUTH_MODEdevbase status表示、起動ログ、ドキュメントすべて未解決レビュースレッド 0 件で収束しています。
移行にあたって
devbase build --no-cache)。entrypoint と Dockerfile を変更していますdefaultグループでは初回起動時に/persistent/ai→/persistent/groupのコピー(実測 1.3GB 程度)が 1 回だけ走ります。初回だけ起動が伸びますdefaultを含む全グループで初回 1 回の認証が必要ですTest plan(結合観点)
defaultと非defaultの 2 グループを同時起動し、gcloud auth listが互いのアカウントを見せないこと(AC3)readlink -f ~/.claude/pluginsが同一の/persistent/ai/.claude/pluginsを指すこと(AC4)devbase build --no-cache後、down→upで gcloud / gws の再認証が発生しないこと(AC1 / AC2)DEVBASE_ACCOUNT_GROUP未設定)がdefaultとして起動し、Claude Code が未ログイン状態にならないこと(AC5 / AC8)~/.claude/CLAUDE.md/settings.jsonが壊れていない symlink で、history.jsonlがディレクトリでないこと(AC6)ubuntu/ 数字のみ / 不正文字)がdevbase upの前に理由つきで拒否されること(AC7)devbase statusに解決されたアカウントグループが出ること(AC10)GCP_AUTH_MODEをadc→key→adcと往復させても壊れないこと(AC11 / AC12 / AC13)docs/user/google-auth.mdの手順だけで未認証グループの認証を通せること(AC14)closes #116