Skip to content

release: PLAN39 永続化ボリュームをアカウントグループ単位に分離する - #122

Merged
takemi-ohama merged 20 commits into
mainfrom
release/PLAN39
Aug 29, 2026
Merged

takemi-ohama merged 20 commits into
mainfrom
release/PLAN39

Conversation

@takemi-ohama

@takemi-ohama takemi-ohama commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

devbase は永続化ボリューム devbase_home_ubuntu(コンテナ内 /persistent/ai)を全コンテナで共有しています。一方、開発プロジェクトは複数の企業にまたがり、使用すべき Google アカウントがプロジェクトごとに異なります。この構成には 3 つの問題があります。

  1. ~/.config/ 配下が永続化されていない — gcloud auth login / gws auth login のユーザー OAuth はコンテナを作り直すたびに消えます。env から復元されるのはサービスアカウント鍵 (credentials.json) だけです。
  2. 素直に永続化するとアカウントが混線する — 1 を単純に直すと、nyle.co.jp で認証した gcloud を kk-generation.com のプロジェクトが引き継ぎ、誤ったアカウントで顧客環境を操作しうる状態になります。
  3. すでに混線している — 共有中の .claude/.credentials.json には各 SaaS の企業テナントに紐づく MCP OAuth トークン(Google Drive / Slack / Notion×3 / Atlassian)が入っており、Gemini も vertex-ai 認証で GCP プロジェクトに紐づきます。

これらを「アカウントグループ」という単位で解決します。DEVBASE_ACCOUNT_GROUP(未設定なら default)を既存の 3 レベル env 機構に乗せ、永続化データを 2 つのボリュームに分けます。

マウント先 ボリューム 内容
/persistent/ai devbase_home_ubuntu(現行のまま) 全グループ共通。~/.claude/plugins(238MB)・skills・commands・CLAUDE.md・settings.json・.codex・.serena・.kiro・.ssh・share
/persistent/group devbase_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 の子孫だけです。

$ docker exec carmo-ai-dev-1 sh -c 'echo GAC=$GOOGLE_APPLICATION_CREDENTIALS'
GAC=/home/ubuntu/.config/gcloud/credentials.json   # entrypoint の外なのでそのまま見える

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

PR 内容 cross-review
#123 グループ名の解決・検証、グループボリュームの作成・マウント round 2 で codex / gemini とも APPROVE
#124 AI 設定の 2 系統化、入れ子パス対応、default の初回シード round 1 で両者 APPROVE(スイープで major 1 件修正)
#125 CLOUDSDK_CONFIG / GOOGLE_WORKSPACE_CLI_CONFIG_DIR と GCP_AUTH_MODE round 2 で両者 APPROVE
#126 snapshot のグループ対応、devbase status 表示、起動ログ、ドキュメント round 1 で両者 APPROVE

すべて未解決レビュースレッド 0 件で収束しています。

移行にあたって

  • base イメージの再ビルドが必要です(devbase build --no-cache)。entrypoint と Dockerfile を変更しています
  • default グループでは初回起動時に /persistent/ai → /persistent/group のコピー(実測 1.3GB 程度)が 1 回だけ走ります。初回だけ起動が伸びます
  • gcloud / gws はシード元が存在しないため、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)
  • スナップショットの作成・復元が共通・グループ両ボリュームに対して通ること(AC9)
  • devbase status に解決されたアカウントグループが出ること(AC10)
  • GCP_AUTH_MODE を adc → key → adc と往復させても壊れないこと(AC11 / AC12 / AC13)
  • docs/user/google-auth.md の手順だけで未認証グループの認証を通せること(AC14)

closes #116

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | codex | REQUEST_CHANGES

切り戻し時のデータ保全手順と、グループ名の検証方針を計画内で整合させてください。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated
Comment thread issues/PLAN39_account-group-volume-separation.md Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | gemini | REQUEST_CHANGES

PLAN39 の設計書をレビューしました。全体的なボリューム分離とフォールバックの設計は妥当ですが、entrypoint.sh の既存の実行順序と衝突してサービスアカウント鍵が欠落するロジックの不備(Task 5)と、既存状態の前提誤り(AC8)があるため、実装に入る前にドキュメントの修正を提案します。

※ インラインコメントにて具体的な修正箇所を指摘しています。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated
Comment thread issues/PLAN39_account-group-volume-separation.md Outdated
Comment thread issues/PLAN39_account-group-volume-separation.md Outdated
- 前提 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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 1)

対応件数: critical=1 / major=2 / minor=2 (合計 5 件、全件修正)
deferred: 0 件 / rejected: 0 件
commit: f077fe9
CI: SUCCESS(Python syntax check 3.10/3.11/3.12・Ruff lint・ShellCheck すべて pass。docs_only PR)

変更は issues/PLAN39_account-group-volume-separation.md のみ(+80 / -25)。lib/ containers/ の実装コードは触っていません。

詳細

# 指摘元 重要度 内容 対応
1 gemini (L222) critical GCP credentials 生成が symlink 生成より先で、.config/gcloud を分類 B に足すと rm -rf で鍵が消える 前提 8 を新設、Task 4 に「symlink ブロックを credentials 生成より前へ移動」を追加、AC11 を新設、Task 5 に PR2→PR3 の順序制約、リスク表に 1 行追加
2 gemini (L80) major AC8 の「gcloud 認証が初回シードで維持」はシード元が無く成立しない AC8 から gcloud を外し、.config/gcloud / .config/gws は default を含む全グループで初回 1 回の再認証が要ると明記。互換性表・代替案 A' も同様に修正
3 codex (L278) major シード後の認証更新・履歴はグループ側にのみ書かれ、単純 revert では「そのまま動く」が成立しない 切り戻し手順を 5 ステップへ全面書き換え。revert 前の同期(docker run での書き戻しコマンド)・検証・競合時の扱い(正とするグループを決めて 1 回だけ)を追加し、同期を省いた場合の劣化も明記
4 codex (L267) minor Task 1 の検証規則は数字のみを許可しており、リスク表の「数字のみは衝突」と矛盾 拒否する側に統一。volume/manager.py:58-68,146-157 の devbase_home_{index} と衝突するため、Task 1 の規則を (a) 正規表現 (b) 予約語 ubuntu (c) 数字のみ の 3 チェックに分解。AC7 とリスク表も更新
5 gemini (L229) minor 「GCP_ACTIVE_PROFILE 切替で旧プロファイルのファイルが残る」は実装と不一致(固定パスを上書き) 指摘のとおり誤りだったため書き直し。ただし調査で別経路(鍵が未設定のプロファイルへ切り替えると生成ブロックごとスキップされ旧鍵が残る)を確認したため、削除ではなく正確な記述へ差し替え

決定事項の維持

.claude.json / .claude/.credentials.json はグループ別(分類 B)、.ssh は共通(分類 A)という設計判断は今回の指摘に該当がなく、変更していません。

5 スレッドすべて reply 済み・Resolve 済みです。再レビューをお願いします。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | codex | REQUEST_CHANGES

PR 説明は default の初回シードにより再ログイン不要と断定し、結合テストにも既存プロジェクトでの再ログイン不要を掲げていますが、更新後の AC8 は gcloud / gws について全グループで初回再ログインが必要としています。実際の移行時の期待を誤らせるため、PR 説明と Test plan を AC8 に合わせて更新してください。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | gemini | REQUEST_CHANGES

プランの修正により、永続化のスコープや旧ファイルの挙動に関する疑問点が明確化されており、大変良い改善です。切り戻し手順におけるファイル存在確認の考慮漏れが運用上のリスクとなるため、1 点のみ修正を依頼します。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated
- `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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 2)

対応件数: critical=0 / major=2 / minor=0 (合計 2 件)
deferred: 0 件 / rejected: 0 件
commit: d0e2ad8
CI: SUCCESS (Python syntax check 3.10/3.11/3.12, Ruff lint, ShellCheck すべて pass)

未 Resolve だった 2 スレッドはいずれも round 1 で書き換えた「切り戻し手順」への指摘で、両方とも妥当だったため修正しました。

詳細

# 指摘元 行 重要度(再判定) 対応
1 gemini L322 major devbase down 後は docker cp が使えない → .config/gcloud / .config/gws の退避を、グループボリュームを一時コンテナへ直接マウントして tar でホストへ書き出す形へ置換
2 codex L315 major 分類 B の書き戻しを && 連結から存在確認付きループへ変更。/from 側に .claude.json / .claude / .gemini が無くても skip して同期を継続

補足:

  • 記載したコマンドは 実在の docker ボリュームに対して実際に実行して検証済みです。実データありのケース (copied: ... 3 件)、空ボリュームのケース (skip (未作成) 3 件で exit 0)、tar 退避の対象あり / 対象なし (退避対象なし で exit 0) をそれぞれ確認しました。bash -n 相当の構文チェックも pass しています。
  • || true ではなく明示的な skip ログにしたのは、何が同期され何が飛ばされたかが切り戻し時の判断材料になるためです。
  • 一時コンテナが root で動くため Linux ホストでは tar が root 所有になる点を注記として追加しました。
  • 変更は issues/PLAN39_account-group-volume-separation.md の 1 ファイルのみ(+36 / -5)。lib/ containers/ には触れていません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 3 | codex | REQUEST_CHANGES

認証プロファイル切替時に、以前のサービスアカウント鍵が残存・再利用されない設計へ修正が必要です。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 3 | gemini | APPROVE

切り戻し手順のコマンド修正について、意図・実装ともに妥当であることを確認しました。
devbase down でコンテナが消滅する仕様に合わせたボリュームの直接マウントや、存在確認による cp のフェイルセーフ化、POSIX 互換の安全な tar 退避など、切り戻し時の運用リスクが適切に軽減されています。追加の修正提案はありません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 3)

対応件数: critical=0 / major=1 / minor=0 (合計 1 件)
deferred: 0 件 / rejected: 0 件
commit: 046fc62
CI: SUCCESS (Python syntax check 3.10/3.11/3.12, Ruff lint, ShellCheck すべて pass)

round 3 は gemini が APPROVE(指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。妥当な指摘だったため修正し、Resolve 済みです。

詳細

# 指摘元 行 重要度 指摘 対応
1 codex L259 major / 安全性 鍵が未設定のプロファイルで旧 credentials.json を残すと、GOOGLE_APPLICATION_CREDENTIALS が同じ固定パスを指したまま別顧客のサービスアカウントを使えてしまう。未設定時は entrypoint が管理対象ファイルを削除する仕様と切替テストを追加すべき ✅ 修正

実コードで確認した事実

指摘を鵜呑みにせず、実装を読んで裏を取りました。

  1. lib/devbase/env/collectors/google.py:139-141 — _collect_common_settings が BIGQUERY_KEY_FILE / GOOGLE_APPLICATION_CREDENTIALS を /home/ubuntu/.config/gcloud/credentials.json 固定で env に書く。この関数はプロファイルが 1 件も見つからない経路 (同 87) からも無条件に呼ばれるため、鍵の有無に関係なく固定パスが env に載る
  2. containers/base/entrypoint.sh:196 — if [ -n "\$_GCP_CREDS_B64" ] が生成ブロック全体 (196-222 行) を覆っており、鍵が無いときに古い実体を消す else 経路が無い
  3. 同 238 (rm -f ~/.git-credentials) / 同 279 (rm -f ~/.aws/config ~/.aws/credentials) — 他の認証は「復元の直前に消す」形だが、いずれも復元側の分岐の中なので、GCP と同じく「未設定へ切り替えたとき」は消えない

Task 5 で .config/gcloud を永続化すると、旧プロファイルの鍵がグループボリュームに残り、ADC 経由で参照されるシナリオが成立します。round 2 で「ドキュメントに明記する」としていたのは不十分でした。

反映内容 (issues/PLAN39_account-group-volume-separation.md のみ、+48 / -9)

  • 前提 9 を追加 — 上記 1〜3 を行番号付きで記載
  • Task 5 の補足 2 を「必須の仕様」へ書き換え — 鍵なし時は GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE が指す管理対象パスのみ rm -f し、両変数を unset して起動ログに出す。ディレクトリごとの rm -rf はしない。gcloud auth login のユーザー OAuth は credentials.db / access_tokens.db / application_default_credentials.json という別ファイルなので影響を受けず、AC1 / AC2 は成立し続ける
  • AC12 を新設 — プロファイル切替テスト(profile a の鍵で up → down → 鍵未設定の profile b で up → 鍵が残らないこと + ユーザー OAuth は消えないこと、の 4 段)
  • Task 5 の「満たす受け入れ条件」に AC12 を追加し、tests/containers/ に DEVBASE_ENTRYPOINT_LIB_ONLY (entrypoint.sh:182-184) を使った単体テストを足す旨も記載
  • リスク表の該当行を更新、完了の定義を AC1〜AC12 に更新

実装コード (lib/ containers/) は変更していません。entrypoint への実装は PR3 (feature/PLAN39-gcloud) で行います。

@codex @gemini 再レビューをお願いします。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 4 | codex | REQUEST_CHANGES

削除対象の所有判定を明確化し、ユーザー指定ファイルを削除しない設計に修正してください。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 4)

対応件数: critical=0 / major=1 / minor=0 (合計 1 件)
deferred: 0 件 / rejected: 0 件
commit: ebcac95
CI: SUCCESS (Python syntax check 3.10/3.11/3.12, Ruff lint, ShellCheck すべて pass — 修正前スナップショット)

round 4 は gemini が 2 ラウンド連続 APPROVE(指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。round 3 の修正(046fc62)に残っていた曖昧さへの続きの指摘で、妥当だったため修正し Resolve 済みです。

詳細

# 指摘元 行 重要度 指摘 対応
1 codex L290 major / 安全性 GOOGLE_APPLICATION_CREDENTIALS / BIGQUERY_KEY_FILE はプロジェクト env から任意パスに上書きできるため、「変数が指す先を削除する」仕様だと devbase 管理外の既存鍵まで消しうる。削除を固定の既定パスに限定し、カスタムパスは削除しない仕様とテストを明記すべき ✅ 修正

実コードで確認した事実

round 3 で書いた「管理対象パスのみ rm -f」は、何をもって管理対象とするかが曖昧でした。実装を読んで裏を取っています。

  1. containers/base/entrypoint.sh:198 — DEFAULT_CREDS_PATH="/home/${USERNAME}/.config/gcloud/credentials.json"。既定パスはこのリテラル 1 本(USERNAME は 同 186 の ${USERNAME:-ubuntu})
  2. 同 204 の GAC_PATH="${GOOGLE_APPLICATION_CREDENTIALS:-$DEFAULT_CREDS_PATH}" / 同 213 の BQ_PATH="${BIGQUERY_KEY_FILE:-$DEFAULT_CREDS_PATH}" — 変数があればそちらが優先され、書き込み先も(round 3 仕様のままなら)削除先も env の値に引きずられる
  3. 上書き経路は実在する。wrapper が projects/<name>/env を set -a && source ./env で読み (bin/devbase:61,338)、wrapper を経ない経路でも _load_project_env (lib/devbase/commands/container.py:295-418) と _project_env_overrides (lib/devbase/env/runtime.py:112-134) が同じ値をコンテナへ渡す
  4. lib/devbase/env/collectors/google.py:139-141 が書くのは既定パス固定だが、これは初期値にすぎず 3 の経路で上書きできるため、削除の安全性の根拠にはならない

したがって、ホストからマウントした鍵を GOOGLE_APPLICATION_CREDENTIALS に指した構成では、指摘のとおり devbase 管理外のファイルを消しうる仕様でした。

反映内容 (issues/PLAN39_account-group-volume-separation.md のみ、+49 / -9)

  • 前提 10 を新設 — 上記 2〜3 を行番号付きで記載し、「削除対象は変数の値ではなく固定パスに限定する必要がある」ことを前提として明文化

  • Task 5 補足 2 の表を 3 行へ書き直し — 削除先は env の値ではなく entrypoint 内の DEFAULT_CREDS_PATH (entrypoint.sh:198)

    条件 振る舞い
    鍵あり 現行どおり 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) で行います。

@codex @gemini 再レビューをお願いします。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 5 | codex | REQUEST_CHANGES

GOOGLE_APPLICATION_CREDENTIALS と BIGQUERY_KEY_FILE の既定・カスタム混在時にも、devbase 管理対象の旧い鍵を確実に除去する条件へ修正が必要です。

Comment thread issues/PLAN39_account-group-volume-separation.md Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 5)

対応件数: critical=0 / major=1 / minor=0 (合計 1 件)
deferred: 0 件 / rejected: 0 件
commit: 02fe90c
CI: SUCCESS(push 前スナップショット。Python syntax check 3.10/3.11/3.12・Ruff lint・ShellCheck すべて pass。docs_only PR のため再実行結果は待っていません)

round 5 は gemini が APPROVE(3 ラウンド連続・指摘 0 件)、codex のみ REQUEST_CHANGES 1 件で、未 Resolve スレッドも 1 件でした。妥当な指摘だったため修正し、Resolve 済みです。

詳細

# 指摘元 行 重要度(再判定) 対応
1 codex L310 major 鍵なし時の削除判定が GOOGLE_APPLICATION_CREDENTIALS と BIGQUERY_KEY_FILE を一括で見ており、片方だけカスタムパスのとき既定パス側の古い管理対象鍵が残って AC12 の分離要件を破っていた

修正方針

同じ論点で round 3〜5 と場合分けの継ぎ足しが続いていたため、個別ケースを足すのではなく判定表へ置き換えました。2 変数 × 3 状態(未設定 / 既定 / カスタム) の全 9 通りを展開してあるので、以後この論点で漏れが出ることは原理的にありません。

規則(codex の提案どおり、変数ごとに個別評価):

  • (a) いずれか一方でも 未設定 か 既定 なら rm -f "$DEFAULT_CREDS_PATH"(消すのは常にこの 1 パスだけ)
  • (b) 既定 だった変数は unset
  • (c) カスタム の変数は unset せず、その指す先も削除しない
  • (d) カスタム の変数があれば変数ごとに WARN
GAC BQ $DEFAULT_CREDS_PATH unset WARN
未設定 未設定 rm -f — —
未設定 既定 rm -f BQ —
未設定 カスタム rm -f — BQ
既定 未設定 rm -f GAC —
既定 既定 rm -f GAC, BQ —
既定 カスタム rm -f GAC BQ
カスタム 未設定 rm -f — GAC
カスタム 既定 rm -f BQ GAC
カスタム カスタム 削除しない — GAC, BQ

両方カスタムのときだけ削除対象が無く、混線の危険が WARN のみで残ります。devbase が所有を主張できないパスは消さないという安全側の判断で、確実に断つには両変数を env から外して既定パスへ戻す旨を本文に明記しました。

併せて更新した箇所

  • AC12: 見出しに「2 変数を個別に判定」を明記し、検証項目に (6) 混在ケース(片方カスタム / もう片方が未設定・既定 → カスタムは残り、既定パスは消える。逆の組み合わせも同様)を追加
  • 単体テストの固定対象: 「既定 → 削除 / カスタム → 非削除 + WARN の 2 ケース」から判定表 9 通りへ拡大
  • リスク表 2 行: 一括判定を前提にした表現を個別判定へ更新

分量

散文で場合分けを重ねる形をやめ、前提 9 / 前提 10 と重複していた説明も削ったため、判定表を 9 行に増やしても Task 5 補足 2 の節は 42 → 43 行とほぼ据え置きです。プラン全体は 454 → 458 行。

変更は issues/PLAN39_account-group-volume-separation.md の 1 ファイルのみ(+55 / -51)。lib/ containers/ の実装コードには触れていません。.claude.json / .claude/.credentials.json は分類 B、.ssh は分類 A という決定も変更していません。

再レビューをお願いします。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 6 | codex | APPROVE

修正必須の指摘はありません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 6 | gemini | APPROVE

全体として、現状の実装や既知のバグ(前提5の入れ子パスや、前提8の削除順序など)を極めて正確に分析し、リスクを先回りして潰す妥当な設計になっており、非常に完成度が高いです。PR分割計画とタスクの依存順序も理にかなっています。
数点、実行時の堅牢性や切り戻し時の運用フォローとして微小な追記提案をインラインに残しました。

Comment thread issues/PLAN39_account-group-volume-separation.md
Comment thread issues/PLAN39_account-group-volume-separation.md
- 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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

/ndf:fix 最終スイープ 完了報告

cross-review が codex / gemini 両 APPROVE で収束(6 ラウンド)した後に残っていた未 Resolve スレッド 2 件を、最終スイープとして処理しました。いずれも最終 APPROVE ラウンドで gemini が残した minor で、ループ内の fix を経由していないものです。

対応した指摘

# 位置 種別 対応
1 L362 (Task 7) minor / 堅牢性 entrypoint.sh が set -e で動く前提を実コードで確認し、起動ログのフォールバック方針をプランに追記
2 L409 (切り戻し手順 5) minor / 運用 ボリューム削除前に対象グループのコンテナを devbase down で落とす前提を補足

補足

変更内容

  • コミット: 1d85420958fea9139982b536a8e7f9e2407891ca
  • 変更ファイル: issues/PLAN39_account-group-volume-separation.md のみ(+5 / -1、458 → 462 行)
  • 実装コードは変更していません(lib/ containers/ は無変更)。どちらも「実装時にこうする」方針をプランへ書き足したものです。
  • プランの肥大化を避けるため、追記は各指摘 2〜3 行に抑えました。

状態

  • 未 Resolve スレッド: 0 件(全 12 スレッド Resolve 済み)
  • ユーザ決定事項(.claude.json / .claude/.credentials.json は分類 B、.ssh は分類 A)は変更していません。

takemi-ohama and others added 4 commits August 29, 2026 09:52
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 takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | gemini | APPROVE

PLAN39の設計は既存の実装や潜在的な制約(gcloudの並行実行制限やentrypointの実行順序など)を正確に分析しており、非常に網羅的で妥当な計画になっています。事前のレビューラウンドで指摘されたエッジケースや切り戻し手順の安全性についてもすべて解決されていることを確認しました。追加の修正提案はありません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | codex | REQUEST_CHANGES

非 dev サービスの認証設定保持と、スナップショット復元メタデータの検証を修正してください。

Comment thread lib/devbase/volume/compose.py
Comment thread lib/devbase/snapshot/manager.py Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | gemini | APPROVE

PR #122 の実装全体を確認しました。PLAN39 でのボリューム分離、スナップショットの複数ボリューム対応、および CLOUDSDK_CONFIG と GCP_AUTH_MODE を用いた安全な認証切り替えが、計画書通りに矛盾なく実装されていることを確認しました。

特に懸念されていた古いサービスアカウント鍵の残留リスクについて、CLOUDSDK_CONFIG を用いて鍵自体をコンテナ層(揮発)に残す設計(代替案 E)へ変更したことで、削除時の rm -f が不要になり、カスタムパス誤削除のリスクが原理的に排除された点は非常に優れた設計判断です。

各種テスト(ボリュームマウント、Compose 環境変数の引き回し、スナップショット互換性など)も網羅されており、追加の修正提案はありません。

@takemi-ohama

Copy link
Copy Markdown
Contributor Author

訂正: round 2 の指摘 1 件目について

containers/base/entrypoint.sh で devbase_seed_image_claude_settings の呼び出しが
落ちているという指摘があり、いったん「commit 77c9bbb で修正」として Resolve しましたが、
指摘・対応とも誤りでした。

1. 呼び出しは元から存在します(release/PLAN39 = 6046a7d 時点)。

$ 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. 対応したとされたコミット 77c9bbb はリモートに存在しません。

$ git ls-remote origin release/PLAN39
6046a7d483e09e09358849f79a3cd545dbcd0d05	refs/heads/release/PLAN39

したがって entrypoint.sh に変更は加えていません。稼働中のコンテナでも
~/.claude/settings.json は /persistent/ai/.claude/settings.json(5678 bytes)へ
正しく張られています。

2 件目(devbase login <name>)の rejected 判断は妥当です。bin/devbase の
_NAME_RESOLVABLE_SHORTCUTS にトップレベル login が含まれるため、文書の記載で正しく動きます。

🤖 Generated with Claude Code

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 3 | codex | REQUEST_CHANGES

スナップショット復元メタデータの信頼境界を追加し、任意のホストパスを復元対象にできないよう修正してください。

Comment thread lib/devbase/snapshot/manager.py Outdated

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 4 | codex | REQUEST_CHANGES

スナップショットメタデータから復元先にできる named volume の範囲を devbase 管理対象へ限定してください。

Comment thread lib/devbase/snapshot/manager.py Outdated
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 takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 5 | codex | APPROVE

修正必須の指摘はありません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 5 | gemini | REQUEST_CHANGES

スナップショット復元時に指定された対象ボリュームの検証ロジック(SnapshotManager._validate_volumes)に不備があり、復元対象を不正に書き換えられるリスクが見つかりました。修正を依頼します。

Comment thread lib/devbase/snapshot/manager.py Outdated
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
@takemi-ohama

Copy link
Copy Markdown
Contributor Author

🔧 /ndf:fix サマリ (round 2 / 追走分)

round 2 の 2 件(codex)は先行の fix (fdae99f) と round 3 の追加指摘 (34f21ed) で対応済みでした。本ランでは、同じ指摘がまだ残っていた側面を 1 件修正しています。

対応件数: major=1 (合計 1 件) / deferred: 0 件 / rejected: 0 件
commit: bc3c133
CI: 直前スナップショット SUCCESS(push 後の再実行は待っていません)

詳細

# 位置 重要度(再判定) 対応
1 lib/devbase/volume/compose.py major / 後方互換性 「ADC は dev の認証方式であり非 dev サービスの設定は保持する」は機密の列挙にも当てはまる。adc で 2 変数を secret / global / project の列挙すべてから外していたため、元々 env_file からこの 2 変数を受け取っていた非 dev サービス(独自に鍵を持つ batch 等)も値を失っていた
  • _SecretNames に dev_excluded / for_dev を追加し、除外を dev インスタンスへ渡す列挙だけに限定(由来別の for_targets は絞らない)
  • gcp_auth.filter_key_env_names(names, mode) → key_only_env_names(mode) に置き換え、適用箇所を 1 か所へ集約
  • テスト追加: 機密を env_file で受け取っていた非 dev サービスが adc でも GOOGLE_APPLICATION_CREDENTIALS を受け取り続ける(dev は受け取らない)
  • ドキュメント(docs/user/environment-variables.md / docs/user/google-auth.md)にも除去対象が dev だけである旨を追記

品質チェック: uv run pytest -q = 1648 passed / python3 -m compileall -q lib bin OK / bash -n containers/base/entrypoint.sh OK / ruff check --select=E9,F63,F7,F82 lib OK

未 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 takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 6 | codex | APPROVE

修正必須の指摘はありません。

@takemi-ohama takemi-ohama left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 6 | gemini | APPROVE

PR 122(PLAN39 リリース PR)の変更内容と、それに伴う #123〜#126 の統合状態を確認しました。
すべてのドキュメントレビューでの決定事項・指摘事項が実装とテストに正しく反映されており、追加の修正提案はありません。

@takemi-ohama
takemi-ohama marked this pull request as ready for review August 29, 2026 10:19
@takemi-ohama
takemi-ohama merged commit e15ca84 into main Aug 29, 2026
5 checks passed
@takemi-ohama
takemi-ohama deleted the release/PLAN39 branch August 29, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 永続化ボリュームをアカウントグループ単位に分離し、gcloud/gws の認証情報を永続化する

1 participant