diff --git a/containers/base/entrypoint.sh b/containers/base/entrypoint.sh index 86eefdba..aca5829d 100644 --- a/containers/base/entrypoint.sh +++ b/containers/base/entrypoint.sh @@ -205,6 +205,14 @@ DEVBASE_GROUP_SETTINGS=( ".claude.json" ".claude" ".gemini" + ".local/share/kiro-cli" +) + +# 分類 B のうち、既存コンテナのホームから初回だけ取り込むもの。 +# Kiro CLI 2.x は認証状態を ~/.local/share/kiro-cli にも保存するため、symlink へ +# 張り替える前にコピーしないと、初回適用時のログイン状態を失う。 +DEVBASE_GROUP_HOME_SEED_SETTINGS=( + ".local/share/kiro-cli" ) # 分類 A のうち ~/.claude 配下にあるもの (グループ側の .claude から共通側へ張る) @@ -383,7 +391,7 @@ devbase_seed_entry() { # 失われる。gcloud / gws はシード元が存在しないため対象外 (AC8)。 devbase_seed_group_settings() { local ai_root="$1" group_root="$2" group="$3" - local entry + local entry home_seed_entry skip if [ "$group" != "default" ]; then return 0 @@ -391,6 +399,16 @@ devbase_seed_group_settings() { echo "Seeding account group '${group}' from ${ai_root} (first run only)..." for entry in "${DEVBASE_GROUP_SETTINGS[@]}"; do + # ホームから取り込むエントリには /persistent/ai 側の旧保存先が無い。 + # 専用のホームシードへ任せ、存在しないシード元を試行しない。 + skip=0 + for home_seed_entry in "${DEVBASE_GROUP_HOME_SEED_SETTINGS[@]}"; do + if [ "$entry" = "$home_seed_entry" ]; then + skip=1 + break + fi + done + [ "$skip" = "1" ] && continue if [ "$entry" = ".claude" ]; then devbase_seed_entry "$ai_root/$entry" "$group_root/$entry" \ "${DEVBASE_SHARED_CLAUDE_SETTINGS[@]}" @@ -433,6 +451,12 @@ devbase_setup_ai_settings() { # symlink の中身へコピーしてしまう。 devbase_seed_image_claude_settings "$home_root" "$ai_root" devbase_seed_group_settings "$ai_root" "$group_root" "$group" + for entry in "${DEVBASE_GROUP_HOME_SEED_SETTINGS[@]}"; do + # 既存リンクは以前選択したグループを指す可能性がある。リンク先を辿って + # 新しいグループへ認証状態をコピーしてはならない。 + [ -L "$home_root/$entry" ] && continue + devbase_seed_entry "$home_root/$entry" "$group_root/$entry" + done for entry in "${DEVBASE_SHARED_SETTINGS[@]}"; do devbase_link_setting "$home_root/$entry" "$ai_root/$entry" "$owner" diff --git a/containers/base/tmux.conf b/containers/base/tmux.conf index a700d7bf..c5824a8f 100644 --- a/containers/base/tmux.conf +++ b/containers/base/tmux.conf @@ -9,10 +9,21 @@ # その履歴へ実質的に手が届かないため、以下を既定にする。 # ホイール操作を tmux へ渡す。転がすと自動で copy-mode に入り履歴を遡れる。 -# 選択してボタンを離せば OSC 52 でクリップボードへ入る (set-clipboard の既定は -# external)。ペインをまたいで選択したいときは Shift + ドラッグで端末側の選択に戻る。 +# ドラッグ終了後も選択を保持し、Ctrl+C で OSC 52 経由のクリップボードへコピーする。 +# ペインをまたいで選択したいときは Shift + ドラッグで端末側の選択に戻る。 set -g mouse on +# Windows の選択操作に近づける。ドラッグ終了時には選択を残し、Ctrl+C でコピーする。 +# copy-mode 中も履歴上で新しい範囲をドラッグできるよう、選択開始操作を明示する。 +unbind-key -T copy-mode MouseDragEnd1Pane +unbind-key -T copy-mode-vi MouseDragEnd1Pane +bind-key -T copy-mode MouseDown1Pane select-pane +bind-key -T copy-mode-vi MouseDown1Pane select-pane +bind-key -T copy-mode MouseDrag1Pane select-pane \; send-keys -X begin-selection +bind-key -T copy-mode-vi MouseDrag1Pane select-pane \; send-keys -X begin-selection +bind-key -T copy-mode C-c send-keys -X copy-selection-and-cancel +bind-key -T copy-mode-vi C-c send-keys -X copy-selection-and-cancel + # 既定の 2000 行はビルドログ 1 回で流れ切る。pane あたりの上限であり、実際に使った # 分だけ確保するので固定の先取りは無い。 set -g history-limit 100000 diff --git a/docs/specifications/kiro-auth-persistence-and-tmux-copy.md b/docs/specifications/kiro-auth-persistence-and-tmux-copy.md new file mode 100644 index 00000000..24247358 --- /dev/null +++ b/docs/specifications/kiro-auth-persistence-and-tmux-copy.md @@ -0,0 +1,76 @@ +# Kiro CLI 認証永続化と tmux コピー操作 + +## 概要 + +devbase コンテナは、Kiro CLI 2.x の認証状態と実行データをアカウントグループ単位で永続化する。 +また、tmux の履歴をマウスで遡り、履歴上の文字列をドラッグ選択して `Ctrl+C` でコピーできる +既定設定を提供する。 + +## 対象範囲 + +- Kiro CLI が `~/.local/share/kiro-cli` に保存する状態 +- base イメージに `/etc/tmux.conf` として配置する tmux の既定設定 +- Kiro CLI のログイン方式と認証方式は変更しない +- `Ctrl+Home` の tmux キー割り当ては定義しない + +## 仕様 + +### Kiro CLI の状態 + +`~/.local/share/kiro-cli` は、現在のアカウントグループに対応する +`/persistent/group/.local/share/kiro-cli` へのシンボリックリンクとする。Kiro CLI の認証状態は +AWS アカウントに紐づくため、全コンテナ共通の `/persistent/ai` には保存しない。 + +初回適用時にホーム側へ既存データがあり、グループ側の保存先がまだ存在しない場合は、リンクを +作成する前に既存データをグループ側へコピーする。グループ側にデータがある場合は上書きせず、 +ホーム側が別グループへのシンボリックリンクである場合もリンク先をシード元として使用しない。 + +常に次の条件を保つ。 + +- 共通 AI 資産は `/persistent/ai`、アカウントに紐づく認証状態は `/persistent/group` に置く。 +- 既存の永続データを起動時のシードで上書きしない。 +- 異なるアカウントグループ間で Kiro CLI の状態を共有しない。 + +### tmux の履歴選択とコピー + +base イメージの tmux は `mouse on` を既定とし、マウスホイールで copy-mode に入って履歴を +遡れるようにする。copy-mode と copy-mode-vi の双方で、履歴上のドラッグ開始時に新しい選択を +開始し、ボタンを離した後も選択範囲を保持する。 + +選択中の `Ctrl+C` は `copy-selection-and-cancel` を実行する。tmux は選択内容を OSC 52 経由で +端末のクリップボードへ送り、copy-mode を終了する。コピーせずに選択を終了する場合は `q` を +使用する。ペインをまたぐ端末側の選択には `Shift` + ドラッグを使用する。 + +`Ctrl+Home` は VS Code や Windows 側の操作と競合するため、tmux では割り当てない。 + +## データ・設定 + +| 対象 | 保存先または設定 | 共有範囲 | +| --- | --- | --- | +| Kiro CLI 2.x | `/persistent/group/.local/share/kiro-cli` | 同じアカウントグループ | +| tmux 既定設定 | `/etc/tmux.conf` | base イメージから作成したコンテナ | + +tmux は `/etc/tmux.conf` の後に `~/.tmux.conf` を読むため、利用者は個人設定で既定値を上書き +できる。`~/.tmux.conf` 自体は永続化対象ではない。 + +## 運用 + +- Kiro CLI の状態はコンテナを再作成しても同じグループ volume に残る。 +- アカウントグループを切り替えると、新しいグループの保存先へリンクを張り直す。 +- tmux 設定の変更はイメージ再ビルドとコンテナ再作成後に適用される。稼働中セッションには + `tmux source-file /etc/tmux.conf` で読み込める。 +- 永続化を切り戻す場合も、コピー済みの Kiro CLI データは削除しない。 + +## テスト観点 + +- Kiro CLI のホームパスがグループ volume へのシンボリックリンクになること。 +- 初回の既存ホームデータを保存し、既存のグループデータを上書きしないこと。 +- グループ間および同一ホームでのグループ切り替え時に状態が混ざらないこと。 +- Kiro CLI のホーム由来データを `/persistent/ai` からシードしようとしないこと。 +- tmux が `mouse on` を解釈し、両copy-modeでドラッグ選択保持と `Ctrl+C` コピーを提供すること。 +- `Ctrl+Home` のbindingが追加されていないこと。 +- `~/.tmux.conf` が `/etc/tmux.conf` の既定値を上書きできること。 + +## 関連リンク + +- [コンテナ操作ガイド](../user/container-operations.md) diff --git a/docs/user/container-operations.md b/docs/user/container-operations.md index 12ac6d55..8e2b249f 100644 --- a/docs/user/container-operations.md +++ b/docs/user/container-operations.md @@ -144,7 +144,7 @@ devbase のコンテナは 4 種類のボリュームを使用します。 | ボリューム名 | マウント先 | 共有範囲 | 用途 | |-------------|-----------|---------|------| | `devbase_home_ubuntu` | `/persistent/ai` | 全コンテナで共有 | 契約やテナントに紐づかない共通資産(`~/.claude/plugins` / `skills` / `commands` / `CLAUDE.md` / `settings.json`、`.codex` / `.serena` / `.kiro`、SSH 鍵、共有ファイル置き場 `share`)| -| `devbase_home_{group}` | `/persistent/group` | 同じアカウントグループのコンテナで共有 | 企業テナントに紐づくもの(Claude Code の認証と会話ログ、`.gemini`、gcloud / gws の設定ディレクトリ)| +| `devbase_home_{group}` | `/persistent/group` | 同じアカウントグループのコンテナで共有 | 企業テナントに紐づくもの(Claude Code の認証と会話ログ、`.gemini`、Kiro CLI の `.local/share/kiro-cli`、gcloud / gws の設定ディレクトリ)| | `devbase_work_{index}` | `/work` | 同じ index のコンテナで共有(プロジェクト間も共有) | プロジェクトのソースコード、作業ファイル | | `devbase_vscode_{project}_{index}` | `/home/ubuntu/.vscode-server` | 共有しない(コンテナ 1 つに 1 本)| VS Code Server 本体・拡張機能・接続トークン | @@ -270,6 +270,7 @@ AI CLI ツールの設定や認証情報は、コンテナを再生成しても | `.claude.json` | Claude Code の設定(`oauthAccount` を含む)| | `.claude` | Claude Code の認証・会話ログ・セッション状態(上表の共通資産を除く**すべて**)| | `.gemini` | Gemini CLI と Antigravity CLI の設定(`vertex-ai` は GCP プロジェクトに紐づく。Antigravity CLI は `.gemini/antigravity-cli/` 配下を使う)| +| `.local/share/kiro-cli` | Kiro CLI 2.x の認証状態・実行データ | `~/.claude` は `/persistent/group/.claude` への symlink で、**その配下の既定はグループ側**です。 共通資産だけがその中から `/persistent/ai/.claude/` へ張り直されます。既定をグループ側に @@ -455,14 +456,17 @@ OAuth のログインはコンテナを作り直しても残ります。 | 操作 | キー | |------|------| | スクロール | マウスホイール(自動で copy-mode に入る) | -| コピー | ドラッグして離す(クリップボードへ直接入る) | +| 範囲選択 | ドラッグ(ボタンを離しても選択を保持) | +| コピー | 選択中に `Ctrl+C`(クリップボードへ入れて copy-mode を抜ける) | +| 選択解除 | `q` | | copy-mode に入る | `Ctrl-b` `[` | | 履歴内を検索 | copy-mode 中に `Ctrl-r`(上方向)/`Ctrl-s`(下方向) | | copy-mode を抜ける | `q` | | ペインをまたいで選択 | `Shift` + ドラッグ(VS Code のネイティブ選択に切り替わる) | -コピーは追加設定なしで動きます。tmux の `set-clipboard`(既定 `external`)により、選択して -ボタンを離した時点で OSC 52 が送出され、VS Code がクリップボードへ書き込みます。 +ドラッグ終了後も選択範囲が残ります。`Ctrl+C` を押すと tmux が OSC 52 を送出し、VS Code の +クリップボードへ書き込んで copy-mode を抜けます。コピーせず選択を解除する場合は `q` を +押します。 上表の copy-mode のキーは tmux の `mode-keys` に従います。既定は `emacs` ですが、tmux は `EDITOR` / `VISUAL` に `vi` を含む値が入っていると `vi` へ切り替えるため、その場合の検索は diff --git a/tests/containers/test_entrypoint_ai_settings.py b/tests/containers/test_entrypoint_ai_settings.py index 71c14d60..70bd8d67 100644 --- a/tests/containers/test_entrypoint_ai_settings.py +++ b/tests/containers/test_entrypoint_ai_settings.py @@ -73,12 +73,83 @@ def test_group_entries_point_at_the_group_volume(roots): home, _, grp = roots setup(roots, "kkg") - for entry in (".claude.json", ".claude", ".gemini"): + for entry in (".claude.json", ".claude", ".gemini", ".local/share/kiro-cli"): link = home / entry assert link.is_symlink(), f"{entry} が symlink ではない" assert link.resolve() == (grp / entry).resolve() +def test_kiro_cli_data_points_at_the_group_volume(roots): + home, _, grp = roots + setup(roots, "kkg") + + link = home / ".local" / "share" / "kiro-cli" + assert link.is_symlink() + assert link.resolve() == (grp / ".local" / "share" / "kiro-cli").resolve() + + +def test_existing_kiro_cli_data_is_seeded_before_linking(roots): + home, _, grp = roots + source = home / ".local" / "share" / "kiro-cli" + source.mkdir(parents=True) + (source / "data.sqlite3").write_text("logged-in") + + setup(roots, "kkg") + + assert source.is_symlink() + assert (grp / ".local" / "share" / "kiro-cli" / "data.sqlite3").read_text() == "logged-in" + + +def test_existing_group_kiro_data_is_not_overwritten(roots): + home, _, grp = roots + source = home / ".local" / "share" / "kiro-cli" + target = grp / ".local" / "share" / "kiro-cli" + source.mkdir(parents=True) + target.mkdir(parents=True) + (source / "data.sqlite3").write_text("stale") + (target / "data.sqlite3").write_text("refreshed") + + setup(roots, "kkg") + + assert (target / "data.sqlite3").read_text() == "refreshed" + + +def test_kiro_cli_data_is_separate_between_groups(roots, tmp_path): + home_a, ai, group_a = roots + home_b = tmp_path / "home-b" + group_b = tmp_path / "persistent" / "kkg" + home_b.mkdir() + + setup((home_a, ai, group_a), "default") + setup((home_b, ai, group_b), "kkg") + (home_a / ".local" / "share" / "kiro-cli" / "identity").write_text("default") + + assert not (home_b / ".local" / "share" / "kiro-cli" / "identity").exists() + + +def test_switching_groups_in_the_same_home_does_not_copy_kiro_data(roots, tmp_path): + """既存 symlink 経由で前グループの認証状態を次グループへ移さない。""" + home, ai, group_a = roots + group_b = tmp_path / "persistent" / "kkg" + + setup((home, ai, group_a), "default") + (home / ".local" / "share" / "kiro-cli" / "identity").write_text("default") + + setup((home, ai, group_b), "kkg") + + assert (home / ".local" / "share" / "kiro-cli").resolve() == \ + (group_b / ".local" / "share" / "kiro-cli").resolve() + assert not (group_b / ".local" / "share" / "kiro-cli" / "identity").exists() + + +def test_kiro_home_seed_is_not_attempted_from_shared_volume(roots): + """Kiro 2.x のデータはホーム由来なので /persistent/ai を探索しない。""" + _, ai, _ = roots + result = setup(roots, "default") + + assert f"skip (シード元なし): {ai}/.local/share/kiro-cli" not in result.stdout + + def test_claude_defaults_to_the_group_volume(roots): """``~/.claude`` 配下の既定はグループ側。 diff --git a/tests/containers/test_tmux_conf.py b/tests/containers/test_tmux_conf.py index c74e0e39..4d0ae5dc 100644 --- a/tests/containers/test_tmux_conf.py +++ b/tests/containers/test_tmux_conf.py @@ -146,10 +146,19 @@ def test_dockerfile_installs_conf_as_etc_tmux_conf(): f"配置先かパーミッションが想定と違う: {copy_lines[0]}" -def test_conf_does_not_change_key_bindings(): - """条件 9: 既定値の配布に留め、利用者の既知の操作を壊さない。""" +def test_conf_provides_windows_like_copy_bindings(): + """履歴上のドラッグ選択と Ctrl+C を両モードへ設定する。""" directives = [line.strip() for line in TMUX_CONF.read_text().splitlines() if line.strip() and not line.strip().startswith("#")] assert directives, "設定が 1 行も無い" - assert all(line.startswith("set ") for line in directives), \ - f"set 以外の指示が含まれる: {[x for x in directives if not x.startswith('set ')]}" + binding_directives = [line for line in directives if not line.startswith("set ")] + assert binding_directives == [ + "unbind-key -T copy-mode MouseDragEnd1Pane", + "unbind-key -T copy-mode-vi MouseDragEnd1Pane", + "bind-key -T copy-mode MouseDown1Pane select-pane", + "bind-key -T copy-mode-vi MouseDown1Pane select-pane", + r"bind-key -T copy-mode MouseDrag1Pane select-pane \; send-keys -X begin-selection", + r"bind-key -T copy-mode-vi MouseDrag1Pane select-pane \; send-keys -X begin-selection", + "bind-key -T copy-mode C-c send-keys -X copy-selection-and-cancel", + "bind-key -T copy-mode-vi C-c send-keys -X copy-selection-and-cancel", + ]