Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion containers/base/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 から共通側へ張る)
Expand Down Expand Up @@ -383,14 +391,24 @@ 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
fi

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[@]}"
Expand Down Expand Up @@ -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"
Comment thread
takemi-ohama marked this conversation as resolved.
done

for entry in "${DEVBASE_SHARED_SETTINGS[@]}"; do
devbase_link_setting "$home_root/$entry" "$ai_root/$entry" "$owner"
Expand Down
15 changes: 13 additions & 2 deletions containers/base/tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
takemi-ohama marked this conversation as resolved.
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
Expand Down
76 changes: 76 additions & 0 deletions docs/specifications/kiro-auth-persistence-and-tmux-copy.md
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 8 additions & 4 deletions docs/user/container-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 本体・拡張機能・接続トークン |

Expand Down Expand Up @@ -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/<name>` へ張り直されます。既定をグループ側に
Expand Down Expand Up @@ -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` へ切り替えるため、その場合の検索は
Expand Down
73 changes: 72 additions & 1 deletion tests/containers/test_entrypoint_ai_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Comment thread
takemi-ohama marked this conversation as resolved.
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`` 配下の既定はグループ側。

Expand Down
17 changes: 13 additions & 4 deletions tests/containers/test_tmux_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Loading