feat(PLAN70): 作り直しても残るシェルの設定の置き場所 ~/.shellrc.d (#253) - #262
Conversation
- 読み込み器 containers/base/shellrc-dir.sh を /etc/devbase/ へ置き、~/.bashrc から ai-cli-aliases.sh の次に読む - 置き場所のパスをイメージの ENV DEVBASE_SHELLRC_DIR で示す - entrypoint の分類 B の一覧へ .shellrc.d を足し、グループのボリュームへ張る - 回帰テスト、利用者向け文書、CHANGELOG、実装計画のタスク分解 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fix the observed no-fallback behavior when DEVBASE_SHELLRC_DIR points to a missing directory. Item-Id: R1-001 Round: 1 Impl-Runtime: codex Impl-Model: default
改修計画 — devbasex/devbase #262
ラウンド 1(実装 codex)R1-001 —
|
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 採用 | 1 |
なぜ: 置き場所の決定は『変数が指す既存ディレクトリ』と『未設定・空なら ~/.shellrc.d』しか固定されていない。DEVBASE_SHELLRC_DIR が存在しないディレクトリを指すときに ~/.shellrc.d へ戻らず何も読まずに 0 で終わる経路 (${VAR:-default} の既定値は空のときだけ効く) は未固定で、置き場所の決め方を書き換えるとフォールバックが紛れ込みうる。実測では何も出さず rc=0
手順: 1. rcdir (~/.shellrc.d) に a.sh (echo from-home) を置く
2. env={'DEVBASE_SHELLRC_DIR': str(tmp_path / 'missing')} (作らない) で _run('echo "rc=$?"', home) を実行する
3. stdout が 'rc=0\n' で from-home を含まず、stderr が空であることを比較する
R1-002 — containers/base/shellrc-dir.sh#__devbase_shellrc_dir
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | unit | — | claude | 採用 | 1 |
なぜ: 実際の配置では entrypoint.sh が置き場所をアカウントグループのボリュームへの symlink にする (スクリプト冒頭のコメント) が、既存テストの置き場所はすべて実ディレクトリで、symlink の置き場所を辿って中の *.sh を読む代表経路が固定されていない。[ -d ] やグロブの書き方を変える構造改善で symlink を辿らなくなっても検出できない。実測では symlink 先の a.sh が読まれ rc=0
手順: 1. tmp_path / 'volume' に a.sh (echo read-volume) を置き、home / '.shellrc.d' をそこへの symlink として作る (rcdir フィクスチャは使わない)
2. 環境変数を与えずに _run('echo "rc=$?"', home) を実行する
3. stdout の行が ['read-volume', 'rc=0'] で stderr が空であることを比較する
R1-003 — containers/base/shellrc-dir.sh#__devbase_shellrc_dir_loader
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | kiro | 採用 | 1 |
なぜ: failglob/dotglob の片方だけが on の状態で __devbase_shellrc_opts が単一トークンになり、その 1 つだけを復元する経路が固定されていない。既存テストは両方 on か両方 off のみ
手順: 1. rcdir に 10-a.sh(echo read-a)、20-noperm.sh(echo read-noperm)、30-b.sh(echo read-b)を置く
2. 20-noperm.sh を chmod 000 にして読めなくする
3. _run("") で source する
4. stdout が read-a, read-b の順、returncode が 0 であることを比較する(読めないファイルは飛ばし、後続は続く)
R1-004 — containers/base/shellrc-dir.sh#__devbase_shellrc_file
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 採用 | 1 |
なぜ: ループ内の [ -f ] && [ -r ] の判定のうち、既存テストが通るのは『一致なしでグロブが文字列のまま残る』と『*.sh という名前のディレクトリ』だけで、読み取り権限の無い *.sh と、リンク先の無い symlink の *.sh を黙って飛ばし後続を読む経路は固定されていない。実測では chmod 000 の 15-u.sh とリンク切れの b.sh はどちらも何も出さずに飛ばされ、前後のファイルは読まれる
手順: 1. rcdir に 10-a.sh (echo read-a)、15-unreadable.sh (echo read-u、chmod 0o000)、17-dangling.sh (存在しないパスへの symlink)、20-b.sh (echo read-b) を置く
2. root では -r が常に真になるため、os.geteuid() == 0 のときは pytest.skip する (リンク切れの確認は root でも成立するので、必要なら 2 テストに分ける)
3. _run('echo "rc=$?"', home) を実行し、stdout の行が ['read-a', 'read-b', 'rc=0'] で stderr が空であることを比較する
4. 後片付けのため、テストの終わりに権限を 0o644 へ戻す
R1-005 — containers/base/shellrc-dir.sh#__devbase_shellrc_opts
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 採用 | 1 |
なぜ: 利用者の glob 設定を控えて戻す分岐 (shopt -q failglob / shopt -q dotglob の 2 本) は、両方 on と両方 off の組み合わせでしか固定されていない。片方だけ on のとき控えの文字列に名前が 1 つだけ入り、引用符なしの shopt -s $__devbase_shellrc_opts で戻す経路は未固定で、控えの持ち方 (文字列・配列・連想配列) を変える構造改善でいちばん壊れやすい。実測では failglob=on/dotglob=off → fg=on/dg=off、failglob=off/dotglob=on → fg=off/dg=on がそのまま残る
手順: 1. 既存の test_glob_options_are_restored_before_reading の横に、before を 'shopt -s failglob; shopt -u dotglob' と 'shopt -u failglob; shopt -s dotglob' の 2 通りで parametrize するテストを足す
2. rcdir に SHOW を実行する 10-show.sh を置き、読み込み器の後でも SHOW を実行する
3. stdout の行が、読んでいる最中と読んだ後の両方で before の組み合わせどおり (例: ['fg=on','dg=off','fg=on','dg=off']) になることを比較する。stderr が空であることも見る
ラウンド 2(実装 kiro)
R2-001 — containers/base/shellrc-dir.sh#__devbase_shellrc_dir
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| boundary | unit | — | claude | 採用 | 1 |
なぜ: 置き場所のパスとファイル名に空白を含む入力が固定されていない。グロブ "$__devbase_shellrc_dir"/*.sh と "${__devbase_shellrc_files[@]}" の引用符が、配列や展開の書き換えで外れると単語分割で読まれなくなるが、既存テストは空白を含まないパスだけを使う(現状は空白入りのディレクトリ・ファイル名とも読まれ、stderr は空、終了状態 0 であることを確認済み)
手順: 1. tmp_path に空白を含むディレクトリ (例: 'a dir') を作り、その中に空白を含む名前の '10 x.sh' (echo read-space) と '20-b.sh' (echo read-b) を置く
2. DEVBASE_SHELLRC_DIR にそのディレクトリを渡し _run('echo "rc=$?"', home, env=...) で読み込み器を通す
3. stdout の行が ['read-space', 'read-b', 'rc=0'] で、stderr が空であることを比較する
R2-002 — containers/base/shellrc-dir.sh#__devbase_shellrc_files
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 採用 | 1 |
なぜ: [ -f ] && [ -r ] の分岐のうち、実在するファイルを指す symlink の *.sh を読む経路が固定されていない。固定済みなのは指す先の無い symlink(読まない)と symlink のディレクトリ本体だけで、判定を -L や find 相当へ書き換えると実在の symlink ファイルを落としても検出できない(現状は symlink 先の内容が読まれることを確認済み)
手順: 1. 置き場所の外 (tmp_path) に echo read-target を書いた実ファイルを作る
2. rcdir に '20-link.sh' としてそのファイルへの symlink を張り、前後に '10-a.sh' (echo read-a) と '30-b.sh' (echo read-b) を置く
3. _run('', home) を実行し、stdout の行が ['read-a', 'read-target', 'read-b'] で stderr が空であることを比較する
R2-003 — containers/base/shellrc-dir.sh#source
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | integration | — | codex | 採用 | 1 |
なぜ: 既存テストの読み込み先とファイル名は単純な名前だけであり、空白・グロブ文字を含む実ファイルのパスを分割・再展開せず source できる経路は固定されていない。実ファイルと Bash の読み込みのつなぎ目を対象とし、Docker は使わない。作業ディレクトリ内で現行ローダーを実行し、両ファイルの作用が名前順で呼び出し元シェルに残り、終了状態 0・標準エラー空となることを確認した。
手順: 1. tmp_path 配下に rc dir [x] というディレクトリを作り、DEVBASE_SHELLRC_DIR で指定する。
2. 10 first [a].sh に ORDER=first、20 second *.sh に ORDER="$ORDER second" を書く。
3. 既存の _run から公開入口であるローダー全体を source し、その直後の終了状態と ORDER を取得する。
4. 現行で観測した終了状態 0、ORDER の値 first second、標準エラー空を検証する。テスト用の値を比較し、実装テキストや表示メッセージは固定しない。
ラウンド 3(実装 claude)
(採用した改善項目なし)
見送った項目
| ラウンド | 対象 | 兆候・経路 | 理由 |
|---|---|---|---|
| 1 | containers/base/shellrc-dir.sh#unset |
error | 1 ラウンドの採用上限 5 件を超えた |
| 3 | tests/containers/test_shellrc_dir.py#test_unreadable_file_is_skipped_and_later_files_continue |
duplication | 重要度 minor がしきい値 major 未満 |
| 3 | tests/containers/test_shellrc_dir.py#test_option_set_by_a_file_is_kept |
duplication | 重要度 minor がしきい値 major 未満 |
| 3 | tests/containers/test_shellrc_dir.py#rcdir |
magic_value | 重要度 minor がしきい値 major 未満 |
| 3 | containers/base/shellrc-dir.sh#__devbase_shellrc_opts |
duplication | 重要度 minor がしきい値 major 未満 |
…llrc_dir 置き場所が別ディレクトリへの symlink のときに symlink 先の *.sh を読む 代表経路を現状固定する。実配置では entrypoint.sh が置き場所をアカウント グループのボリュームへの symlink にするが、既存テストは実ディレクトリ だけで、この経路が固定されていなかった。対象コードは変更しない。 Item-Id: R1-002 Round: 1 Impl-Runtime: kiro Impl-Model: default
…llrc_dir_loader 読めないファイル (chmod 000) を -r の判定で飛ばし、後続のファイルを読み続けて 終了状態 0 で終える経路を現状固定テストで固定する。root は読めてしまうため飛ばす。 Item-Id: R1-003 Round: 1 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Record silent skipping, continued loading, and successful loader status. Restore unreadable file permissions after the test and retain dangling-link coverage for root. Item-Id: R1-004 Round: 1 Impl-Runtime: codex Impl-Model: default
…_devbase_shellrc_opts failglob/dotglob のどちらか一方だけ on のとき、控えの文字列に名前が 1 つだけ入り、引用符なしの shopt -s $__devbase_shellrc_opts で戻す経路を 現状固定する。読み込み中と読み込み後の両方で before の組み合わせが そのまま残ることを比較し、stderr が空であることも確かめる。 Item-Id: R1-005 Round: 1 Impl-Runtime: kiro Impl-Model: default
…iners/base/shellrc-dir.sh#__devbase_shellrc_dir 置き場所のパスとファイル名に空白を含む入力で、単語分割されずに名前順で 読まれ、stderr が空で終了状態 0 であることを現状固定する。 Item-Id: R2-001 Round: 2 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fix the current behavior of reading an existing symlink target in filename order with no stderr output. Item-Id: R2-002 Round: 2 Impl-Runtime: codex Impl-Model: default
…e names — containers/base/shellrc-dir.sh#source 置き場所・ファイル名にグロブ文字 ([x]) と空白を含む実ファイルを DEVBASE_SHELLRC_DIR で指定し、両ファイルの作用が名前の昇順で呼び出し元シェルに残り、終了状態 0・標準エラー 空になる経路を現状固定した。実装テキストや表示メッセージは固定しない。 Item-Id: R2-003 Round: 2 Impl-Runtime: kiro Impl-Model: default
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | REQUEST_CHANGES
修正が必要な指摘は 1 件です。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | agy | APPROVE
PR #262 の現状固定テストの追加内容を確認し、問題ありません。
…-dir.sh The saved option names were joined into one string and split unquoted on restore, so with an IFS lacking a space (e.g. IFS=$'\n\t') shopt got one bad word and both options stayed off. Keep each option in its own flag variable and restore them one by one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🔧 /ndf:fix サマリ | round 1 | commit 2bb114b対応件数: critical=0 / major=0 / minor=1(合計 1 件) |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | kiro | APPROVE
failglob/dotglob の控えを 1 本の文字列 + 引用符なし分割から、オプションごとの独立フラグ変数 + 個別 shopt -s へ変更する修正。利用者の IFS に空白が無い場合 (IFS=$'\n\t' など) に控えが復元されず、旧コードでは stderr に invalid shell option name を出しつつ両オプションが off のまま残る不具合を解消している。修正は最小で正確、design doc も同期しており、新規回帰テスト (test_glob_options_are_restored_when_ifs_has_no_space) を含む 37 件が通過。旧コードで再現し新コードで解消することも確認した。修正を要する指摘は無し。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | claude | APPROVE
round 3: 追加の修正提案なし(IFS に依存しない glob 設定の復元を含め、読み込み器・entrypoint・Dockerfile・文書の整合を確認。tests/containers/test_shellrc_dir.py 37 件通過)
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | kiro | REQUEST_CHANGES
PLAN70 のシェル設定置き場 ~/.shellrc.d に関する特性テスト群と、IFS 非依存で failglob/dotglob を控えて戻す修正 (2bb114b)。テストは全件 pass し、ローダー本体は空白・グロブ文字・symlink・読めないファイル・IFS 変更に対して堅牢であることを実測でも確認した。設計・正確性の面で追加の修正提案は無い。ドキュメントの誤りの記述だけ、実装(読めないファイルは無言でスキップ)と食い違うため 1 点だけ修正を提案する。
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🔧 /ndf:fix サマリ | commit cc8ea96対応件数: critical=0 / major=0 / minor=0(合計 0 件) |
…#253) PLAN70 の要求・設計を issues/old/ へ移し、置き場所と環境変数の約束・読む先の 決め方・読むものと読む順・グロブの設定の控えと戻し(IFS に依らない)・黙って 飛ばすものと知らせるもの・伝播の規則・テスト観点を shellrc-dir.md として確定 仕様にした。利用者向けの小節から新しい仕様へリンクする。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…c-dir # Conflicts: # CHANGELOG.md
振り返り(2026-09-24)対象: issue #248 / issue #234 / issue #253、PR #263 / PR #264 / PR #262 3 本を 1 つのまとまりとして main へマージした(#263 → #264 → #262)。変更は次の 3 つ。
このリポジトリは検証チャネルを持たないため、main へのマージがそのまま配布にあたる。承認の範囲に従い、タグと GitHub Release は作っていない。 何が起きたか
context window の大きさ
次に変えること
途中で起票した課題
|
Summary
Closes #253
コンテナを作り直しても残るシェルの設定の置き場所
~/.shellrc.d/を用意する。設計はissues/old/PLAN70_shellrc-dir-design.md(#257 でマージ済み)、要求と受け入れ条件・タスク分解はissues/old/PLAN70_shellrc-dir.md。containers/base/shellrc-dir.shを/etc/devbase/shellrc-dir.shへ置き、~/.bashrcからai-cli-aliases.shの次に 1 行で読む(置き場所の定義が devbase の起動定義に勝つ)ENV DEVBASE_SHELLRC_DIR=/home/${USERNAME}/.shellrc.dで示す(非対話のdocker execからも見える)。読み込み器は変数が空なら~/.shellrc.dを読むentrypoint.shの分類 B(DEVBASE_GROUP_SETTINGS)へ.shellrc.dを足し、/persistent/group/.shellrc.dへ張るdocs/user/container-operations.md)と CHANGELOGlfmは対象外(設計の決定 5・8)反映には
devbase build base --no-cacheが要る(devbase upだけでは反映されない)。構造改善(cross-refactoring)では構造の変更の採用は 0 件で、読み込み器の現状固定テストを 8 コミット足した(改修計画: #262 (comment) )。
実装レビュー(cross-review、codex / agy / kiro / claude の 4 者、3 ラウンド)で 2 件を直した: 利用者の
IFSに空白が無いとfailglob/dotglobを戻せなかった件(2bb114b。設定ごとに別の変数へ控えて個別に戻す)と、読めないファイル・リンク切れは何も出さずに飛ばすことの文書への明記(cc8ea96)。Test plan
uv run --locked pytest tests/containers/test_shellrc_dir.py -q→ 37 passed, exit=0(受け入れ条件 4〜10・6a・8・非機能の性能・Dockerfile の配置と順序。ホストの/bin/bash3.2)uv run --locked pytest tests/containers/test_entrypoint_ai_settings.py -q→ 37 passed, exit=0(受け入れ条件 1・3 の関数の部分・13)env -u DEVBASE_ROOT uv run pytest -q -p no:cacheprovider→ 3064 passed, exit=0(受け入れ条件 12・15。cc8ea96、2026-09-24 13:41)./bin/devbase build base --no-cache(arm64)→ exit=0、3 分 17 秒(受け入れ条件 16)。2bb114b(レビューの修正の後。cc8ea96 は文書だけ)から建てたイメージsha256:2078d38bc1f1…、作成 2026-09-24T04:37:44Z。イメージの/etc/devbase/shellrc-dir.shは作業ツリーのファイルと一致(diffで差分なし、644 root)tests/containers/test_shellrc_dir.pyとtest_ai_cli_aliases.py→ 75 passedイメージの中での確認
使い捨ての
docker run(-v <テスト用ボリューム>:/persistent/ai -v <テスト用ボリューム>:/persistent/group -e DEVBASE_ACCOUNT_GROUP=...)で entrypoint を通し、対話シェルはdocker exec -t <c> bash -ic(devbase loginと同じ対話・非ログインのシェル)で開いた。作ったコンテナとボリュームは終了時に消した。受け入れ条件 2・3 は計画では実プロジェクトの
devbase down→devbase upで確かめるとしていたが、検査の時点で利用者のコンテナを起こさない決まりのため、同じ形(アカウントグループのボリュームを保ったままコンテナを作り直す)を使い捨てのコンテナとテスト用のボリュームで確かめた。devbase upがボリュームを/persistent/groupへ付ける経路はこの変更で変わっていない。CI はイメージを建てない。上のイメージの中の確認は手元(arm64)で採った。amd64 では建てていない。
確定仕様化(b881eda)
docs/specifications/shellrc-dir.mdissues/PLAN70_shellrc-dir*.md→issues/old/docs/user/container-operations.mdenv -u DEVBASE_ROOT uv run pytest -qexit=0、CI(b881eda)すべて success。🤖 Generated with Claude Code