From 20c8e1423e8fbde8331303e3c210ec9054f86755 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Fri, 18 Sep 2026 20:50:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs(PLAN62):=20=E6=A9=9F=E5=AF=86=E3=81=AE?= =?UTF-8?q?=E7=BD=AE=E3=81=8D=E5=A0=B4=E3=81=AB=E6=9B=B8=E3=81=84=E3=81=9F?= =?UTF-8?q?=20DEVBASE=5FACCOUNT=5FGROUP=20=E3=82=92=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E8=A6=81=E6=B1=82=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=A8=E8=A8=AD=E8=A8=88=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- issues/PLAN62_inject-account-group-design.md | 248 +++++++++++++++++++ issues/PLAN62_inject-account-group.md | 108 ++++++++ 2 files changed, 356 insertions(+) create mode 100644 issues/PLAN62_inject-account-group-design.md create mode 100644 issues/PLAN62_inject-account-group.md diff --git a/issues/PLAN62_inject-account-group-design.md b/issues/PLAN62_inject-account-group-design.md new file mode 100644 index 00000000..e7faa2ac --- /dev/null +++ b/issues/PLAN62_inject-account-group-design.md @@ -0,0 +1,248 @@ +# PLAN62: 機密の置き場に書いた `DEVBASE_ACCOUNT_GROUP` を注入しない の設計 + +要求と受け入れ条件は [PLAN62_inject-account-group.md](PLAN62_inject-account-group.md) にある。この文書は「どう作るか」だけを扱う。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 機密の置き場(4 つ)にある `DEVBASE_ACCOUNT_GROUP` を合成から外し、プロセスの環境変数・子プロセス・コンテナの列挙のどれにも載せない | devbase の利用者(機密を注入するすべてのコマンド) | +| F2 | 外したことを、置き場の種類と消し方を添えた警告で知らせる。同じ置き場について 1 回の起動で 1 回まで | devbase の利用者(同上) | +| F3 | `devbase env set DEVBASE_ACCOUNT_GROUP=...` を置き場へ書かずに拒否し、`env` ファイルへ書くよう案内する | devbase の利用者(CLI) | +| F4 | 利用者向け文書と CHANGELOG を新しい挙動に合わせる | devbase の利用者(文書の読み手) | + +## 値の経路 + +いまアカウントグループの値は、次の 3 つの経路でプロセスの環境変数 `DEVBASE_ACCOUNT_GROUP` へ届く。ボリュームのグループ(`resolve_account_group()`)はこの環境変数だけを読む。 + +```mermaid +graph LR + S[シェルの環境変数] --> P[プロセスの環境変数] + F[env ファイル
$DEVBASE_ROOT/env
projects/name/env] -->|ラッパーの source| P + B[機密の置き場 4 つ] -->|runtime.resolve → inject| P + P --> V[resolve_account_group
ボリュームのグループ] + F --> G[groups.declare
機密の置き場のグループ] +``` + +この変更は 3 本目の辺(置き場 → `resolve` → プロセス)だけを断つ。残る 2 本は変えない。機密の置き場のグループ(`groups.declare`)は、もともと `env` ファイルだけから決まる(PLAN56)。 + +## 構成要素 + +| 要素 | 変更 | 責務 | +| --- | --- | --- | +| `env/runtime.py` の `_warned_account_group_refs`(新設) | 足す | 警告を出した置き場の参照(`SecretRef`)の集合。モジュールの変数で、プロセスが終わるまで持つ | +| `env/runtime.py` の `_without_account_group(ref, data)`(新設) | 足す | `data` に `DEVBASE_ACCOUNT_GROUP` があれば、そのキーを除いた新しい辞書を返し、警告を出す(`ref` が集合に無いときだけ)。無ければ `data` をそのまま返す。受け取った辞書は変えない | +| `env/runtime.py` の `resolve` | 変える | 4 つの置き場から読んだ辞書を、合成に使う前に `_without_account_group` へ通す。docstring に、このキーを合成しないことと理由を書き足す | +| `commands/env.py` の `cmd_env_set` | 変える | キー名が `DEVBASE_ACCOUNT_GROUP` なら、置き場を開く(`_open_target_env`)前に案内を出して 1 を返す | +| `env/keys.py` の `DEVBASE_ACCOUNT_GROUP` のコメント | 変える | 機密の置き場の値は注入しないこと(PLAN62)を 1 行足す | +| `docs/user/environment-variables.md` | 変える | 「アカウントグループ」の節に、置き場の値は使われず警告が出ることと、`env set` で書けないことを書く | +| `docs/user/env-backend.md` | 変える | 「グループの決まり方」と「`up` / `scale` がグループの食い違いで止まったとき」の、置き場に書いたときの記述を書き替える | +| `docs/user/cli-reference/03-env.md` | 変える | `devbase env set` の節に、`DEVBASE_ACCOUNT_GROUP` は書けないことを 1 行足す | +| `CHANGELOG.md` | 変える | `[Unreleased]` の `### Changed` に 2 項目。置き場の値を `version: 1` でも使わず警告すること(ボリュームのグループが `env` ファイルの宣言へ戻りうることを添える)と、`env set` で拒否すること | + +次のものは変えない。 + +- `runtime.py` の `_project_env_overrides`、`inject`、`child_env`、`release_store`、`SecretEnv` +- `volume/compose.py`(コンテナの `DEVBASE_ACCOUNT_GROUP` は解決済みのグループを書く) +- `volume/manager.py` の `resolve_account_group`、`env/groups.py` +- `commands/container.py` の `_check_group_consistency`、`cli.py` の `_load_secret_env` + +下の図は呼び出しの関係だけを描く。文書(`docs/`・`CHANGELOG.md`)と `keys.py` のコメントは図に含めない。 + +```mermaid +graph TD + L[cli._load_secret_env] --> I[runtime.inject] + C[container._inject_secrets] --> I + X[env exec] --> CE[runtime.child_env] + I --> R[runtime.resolve] + CE --> R + R --> W[_without_account_group] + W --> WS[_warned_account_group_refs] + R --> O[_project_env_overrides] + ES[cmd_env_set] -->|DEVBASE_ACCOUNT_GROUP なら
ここで 1| X1[終了] + ES -->|それ以外| T[_open_target_env] +``` + +## 入出力の契約 + +### `runtime.resolve` の結果 + +| 項目 | 変更後 | +| --- | --- | +| `values` | `DEVBASE_ACCOUNT_GROUP` を含まない。`projects//env` が同じキーを宣言していても含まない(名前の一覧に無いキーは値を採らないため) | +| `global_names` / `project_names` / `names` | `DEVBASE_ACCOUNT_GROUP` を含まない。ほかのキーの並びと重複の畳み方は変えない | +| 置き場への要求 | 変えない(4 参照を 1 回ずつ `store.load`) | +| 比べ方 | キー名の完全一致。`export DEVBASE_ACCOUNT_GROUP` のように接頭辞の付いたキーは別の名前の変数になり、グループに効かないため対象外 | + +`inject` と `child_env` は `resolve` の結果だけを載せるため、どちらも `DEVBASE_ACCOUNT_GROUP` を書き換えない。プロセスの環境変数に元からある値(シェルか `env` ファイル由来)は残る。 + +### 警告 + +`logger.warning` で 1 件出す。値は出さない。 + +```text +機密の置き場({参照の表示})にある DEVBASE_ACCOUNT_GROUP は使いません。アカウントグループは env ファイル(projects//env・$DEVBASE_ROOT/env)で決まります。消すには: devbase env delete DEVBASE_ACCOUNT_GROUP{付ける引数}{実行場所} +``` + +`{参照の表示}` は `SecretRef.label()` をそのまま使う。`{付ける引数}` と `{実行場所}` は参照から組む。 + +| 置き場 | `label()` の例 | 付ける引数 | 実行場所 | +| --- | --- | --- | --- | +| チーム共通 | `グローバル` | なし | なし | +| 個人共通 | `個人のグローバル` | ` --user` | なし | +| プロジェクトのチーム | `プロジェクト 'web'` | ` -p` | `(projects/web で実行)` | +| プロジェクトの個人 | `個人のプロジェクト 'web'` | ` -p --user` | `(projects/web で実行)` | + +参照がグループを持つとき(`layout: group`)は、`label()` の末尾に `(グループ with)` が付き、付ける引数の末尾に ` --group with` を足す。`--group` の名前は参照が持つ読み替え前の名前である。 + +| 項目 | 内容 | +| --- | --- | +| 出る回数 | 同じ参照(`SecretRef` の値が等しいもの)について、1 プロセスで 1 回まで。`release_store` をまたいでも出し直さない | +| 出ない条件 | 置き場に `DEVBASE_ACCOUNT_GROUP` が無い。空の値でもキーがあれば出す | +| 出る場所 | 注入を行うすべてのコマンド。`version: 1` では `env delete` 自身の実行前の注入でも 1 回出る(消す操作の直前の知らせになる) | + +### コマンド `env set` + +| 項目 | 内容 | +| --- | --- | +| 名前 | `devbase env set KEY=VALUE [-p] [--user] [--group NAME]` | +| 変わる入力 | `KEY` が前後の空白を除いて `DEVBASE_ACCOUNT_GROUP` と一致するとき | +| 出力(拒否) | 終了コード 1。error ログ `DEVBASE_ACCOUNT_GROUP は機密の置き場へは書けません(置き場の値はアカウントグループの決定に使われません)。projects//env か $DEVBASE_ROOT/env に書いてください` | +| 置き場への作用 | 無し。`_open_target_env` より前に返すため、ファイルを作らず、サーバへ要求しない | +| 引数の検査との順序 | `-p` / `--user` / `--group` の検査より前に拒否する。`--group` に使えない名前を渡しても終了コードは 1 | +| 互換性 | 変わる。これまで書けたキーが書けなくなる。CHANGELOG の「変更」に書く。`env import` / `env edit` / 他のキーの `env set` は変わらない | + +## 処理の流れ + +`resolve` の中の順序を、重ね順の番号とともに描く。外すのは 1・2・4・5 の読み取りの直後で、3 は今のまま通す。 + +```mermaid +graph TD + A[1 チーム共通を load] --> A2[_without_account_group] + A2 --> B[2 個人共通を load] + B --> B2[_without_account_group] + B2 --> P{project あり} + P -->|なし| Z[names と values を組む] + P -->|あり| E[3 _project_env_overrides
merged だけへ重ねる] + E --> D[4 プロジェクトのチームを load] + D --> D2[_without_account_group] + D2 --> U[5 プロジェクトの個人を load] + U --> U2[_without_account_group] + U2 --> Z + Z --> Q[values は names にあるキーだけ] +``` + +`_without_account_group` の中では、キーがあり参照が集合に無いときだけ警告を出して集合へ足す。 + +### 重ね順 3 と `env` ファイルの値 + +`_project_env_overrides` は `projects//env` のキーについて、プロセスの環境変数の値を `merged` へ重ねる。`values` は `names`(4 つの置き場のキー)にあるキーだけを `merged` から採る。外した後は `DEVBASE_ACCOUNT_GROUP` が `names` に無いため、`merged` に `env` ファイルの値が入っても `values` には現れない。 + +プロセスの環境変数の `DEVBASE_ACCOUNT_GROUP` は、ラッパーの `source`(または `_load_project_env`)が載せた値とシェルの値のままになる。`inject` はそれを書き換えない。これが受け入れ条件 2 の「`with` のまま変わらない」にあたる。重ね順 3 に手を入れる必要はない。 + +変更前は、プロジェクトの置き場(重ね順 4・5)の値が重ね順 3 に勝っていた。そのため `projects//env` がグループを宣言していても、置き場の値でプロセスの環境変数が上書きされた。この順は既存のテストが任意のキーで固定している(`tests/env/test_runtime.py` の `test_project_env_beats_global_layers_and_loses_to_project_layers`)。 + +### `devbase up` での順序 + +```mermaid +sequenceDiagram + participant W as bin/devbase + participant L as cli._load_secret_env + participant U as cmd_up + participant R as runtime.resolve + participant K as _check_group_consistency + W->>W: env ファイルを source + W->>L: python -m devbase.cli up + L->>R: inject(置き場の値を外す。警告はここで 1 回) + L->>U: dispatch + U->>K: _run_pre_up_checks の冒頭 + K->>K: resolve_account_group と groups.declare を比べる + K-->>U: 食い違えば False で終了コード 1 + U->>R: _build_scaled_override の _inject_secrets(警告は出ない) + U->>U: 構成の生成で解決済みのグループを environment へ書く +``` + +`up` は同じプロセスの中で `resolve` を 2 回以上呼ぶ。その間の `_ensure_env_files` は、子プロセスの `env init` の後に `release_store` を呼ぶ。警告の集合はストアと別に持つため、2 回目以降の `resolve` では出ない。 + +## 決定の記録 + +### 決定 1: 外す場所は `resolve` の 4 つの読み取りの直後の 1 か所にする + +`inject`・`child_env`・コンテナへ列挙する変数名(`SecretEnv.names`)は、どれも `resolve` の結果から作られる。読み取りの直後で外せば、3 つの経路と重ね順の全層に同じ規則が当たり、経路を足しても漏れない。 + +`inject` と `child_env` のそれぞれで外す形は採らない。コンテナの列挙(`names`)に残り、Compose が devbase のプロセスの値を読むことになるためである。ボリュームの側を `groups.declare` へ揃える形も採らない。PLAN56 がスナップショット・`status`・entrypoint へ渡す値まで経路が変わるとして退けており、要求の対象範囲でも「含まない」としている。 + +### 決定 2: `version: 1` でも外す + +利用者向け文書は PLAN56 以降、置き場に書いた値はグループの決定に使われないと説明している。`version: 1` だけ置き場の値が効く今の挙動は、この説明と食い違う。置き場の値でグループを切り替える使い方は文書に無い。 + +`layout: group` のときだけ外す形は採らない。設定の版でグループの決まり方が変わり、文書に版ごとの例外を書くことになるためである。 + +### 決定 3: 置き場の値は消さず、警告で知らせる + +置き場は他のメンバーと共有する場所(チームの参照)でもあり、コマンドの実行の副作用で書き換えると、書いた本人が知らないまま他の端末にも効く。消すかどうかは利用者が決め、消し方を警告に添える。自動削除は要求の対象範囲で「行わない」としている。 + +### 決定 4: 警告の重複抑止は、置き場の参照を鍵にしたモジュールの集合で、プロセスの寿命だけ持つ + +`resolve` は 1 回の起動で何度も呼ばれる。 + +| 呼び出し元 | 経路 | +| --- | --- | +| `cli._load_secret_env` | dispatch 前の注入 | +| `container._inject_secrets` | `up` / `down` / `scale` などの各コマンド | +| `runtime.child_env` | `env exec` | + +`SecretRef` は凍結した dataclass で、種類・プロジェクト名・持ち主・グループを持つため、そのまま「同じ置き場」の鍵になる。 + +`SecretStore` に持たせる形は採らない。ストアは `release_store` で捨てられる(`_dispatch_lifecycle` の出口、`up` の中の `env init` の後、TUI の操作の入口)。そのため `up` 1 回の中で警告が 2 回出て、受け入れ条件 4 を満たさない。`release_store` で集合も消す形も同じ理由で採らない。 + +TUI は 1 プロセスで操作を続けるため、同じ置き場の警告は TUI を起動して最初に注入したときの 1 回だけになる。受け入れ条件 4 は「1 回の CLI 起動で」を単位にしており、これに合う。 + +### 決定 5: 消し方の引数は参照から組み、グループを持つ参照には常に `--group` を付ける + +`env delete` の宛先は `-p`(適用範囲)・`--user`(持ち主)・`--group`(グループ)の 3 軸で決まる。3 軸は参照の 3 つのフィールドと 1 対 1 に対応する。`--group` を省くと、`env delete` は実行した場所のグループ(`$DEVBASE_ROOT/env` など)を宛先にし、警告を出した置き場と違う置き場を指すことがある。共通の参照でも付けて、どこで打っても同じ置き場を指すようにする。 + +プロジェクトの参照では `-p` がプロジェクトのディレクトリでしか使えないため、実行場所を添える。 + +### 決定 6: 重ね順 3(`_project_env_overrides`)は変えない + +重ね順 3 は、置き場のキーのうち `projects//env` にもあるものの値を差し替える働きである。キーの一覧(`names`)は置き場からだけ作られるので、置き場から外したキーには働かない。`env` ファイル由来の値はラッパーの `source` でプロセスに載っており、`inject` が触らなければそのまま残る。 + +`_project_env_overrides` の結果からも `DEVBASE_ACCOUNT_GROUP` を除く形は採らない。`values` に現れない値を除いても結果が変わらず、同じ規則が 2 か所に分かれる。 + +### 決定 7: `env set` は置き場を開く前に拒否する + +`layout: group` の `--group` の検証や、サーバ backend での現物の読み出し(`fresh`)は、置き場を開く `_open_target_env` の中で行われる。その前に返せば、拒否する操作でサーバへの要求もファイルの作成も起きない。拒否の理由は宛先によらないため、引数の組み合わせの検査より先に置く。 + +`env import` と `env edit` は拒否しない(要求の前提 4)。どちらも複数のキーをまとめて扱い、1 キーのために全体を止めると他のキーの作業まで止まる。書かれた値は決定 3 の警告で知らせる。 + +### 決定 8: 受け入れ条件 6 は、注入と食い違いの検査を実際の順に呼ぶ単体テストで確かめる + +`up` で置き場の値が効くのは、書く側と読む側の順序による。dispatch 前の注入(`cli._load_secret_env`)が先にプロセスの環境変数を書く。その後、`_run_pre_up_checks` の冒頭で `_check_group_consistency` が読む。既存の `cmd_up` のテストの足場(`mismatch` / `up_harness`)は注入と検査のどちらかを差し替えており、この順序を通らない。偽の OpenBao(`layout: group`)に値を置き、`runtime.inject` の後に `container._check_group_consistency()` を呼ぶ。 + +`cmd_up` を丸ごと通す形は採らない。dispatch 前の注入は `cmd_up` の外(`cli.py`)にあり、`cmd_up` から呼んでも同じ順序にならない。 + +## テスト設計 + +新しいテストはすべて `DEVBASE_ROOT` を tmp へ向ける(`monkeypatch.setenv`)。プロセスの `DEVBASE_ACCOUNT_GROUP` は、前提どおりに置くか外す。警告の集合は各テストの先頭で `monkeypatch.setattr(runtime, '_warned_account_group_refs', set())` で空にする。 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| 1 | 単体(`tests/env/test_runtime.py`): age と plaintext の各 backend で、チーム共通に `DEVBASE_ACCOUNT_GROUP=kkg` と他のキーを置き、`DEVBASE_ACCOUNT_GROUP` を外した `os.environ` へ `inject(root, 'web', store=store)`。`os.environ` に `DEVBASE_ACCOUNT_GROUP` が無く、他のキーは載り、`resolve_account_group()` が `default` | +| 2 | 単体(同上): 1 と同じ置き場で `DEVBASE_ACCOUNT_GROUP=with` を置いて `inject`。値が `with` のまま。`projects/web/env` に `DEVBASE_ACCOUNT_GROUP=with` がある場合(重ね順 3 が働く場合)も同じ | +| 3 | 単体(同上): `_FourLayerStore` で 4 つの置き場を 1 つずつ(parametrize)置き、1・2 と同じ結果。プロジェクトの置き場(4・5)に `kkg`、`projects/web/env` と `os.environ` に `with` のときも `with` のまま | +| 4 | 単体(同上): caplog で、警告に `DEVBASE_ACCOUNT_GROUP`・参照の表示・`devbase env delete DEVBASE_ACCOUNT_GROUP` と置き場に応じた引数(4 種と、グループを持つ参照の `--group`)を含み、値(`kkg`)を含まない。`resolve` を 2 回呼び、間に `release_store` を挟み、別の `store` を渡しても、同じ参照の警告は 1 件。別の参照は別に 1 件 | +| 5 | 単体(同上): `resolve` の `names` / `global_names` / `project_names` / `values` に `DEVBASE_ACCOUNT_GROUP` が無い。コンテナの列挙は `names` から作られるため、構成の生成は変えずにこれで確かめる | +| 6 | 単体(`tests/commands/test_container_up_order.py`): `openbao_root` を `configure_openbao(layout='group', group_aliases={'default': 'nyle'})` にし、`team/nyle/global` に `DEVBASE_ACCOUNT_GROUP=kkg` を置く。`projects/web` に `env` を置かず、`DEVBASE_ROOT` と `PWD` を向け、`DEVBASE_ACCOUNT_GROUP` を外す。`runtime.inject(root, 'web')` の後に `container._check_group_consistency()` が True | +| 7 | 単体(`tests/commands/test_env_account_group.py`、新設): `cmd_env_set(root, 'DEVBASE_ACCOUNT_GROUP=kkg')` と、`project=True`・`user=True`・`group='kkg'` の各組み合わせで 1。偽の OpenBao に書き込みの要求(POST)が無く、ファイル backend では `.env` が作られない。error ログに `env` ファイルの案内がある | +| 8 | 既存の `tests/env/test_runtime.py`・`tests/env/test_runtime_store.py`・`tests/cli/test_secret_injection.py` が変更なしで通る | +| 9 | `uv run pytest tests/ -q` と `ruff check --select=E9,F63,F7,F82 lib` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 置き場に値を書いている利用者の有無 | 置き場に `DEVBASE_ACCOUNT_GROUP` を書いて `version: 1` でグループを切り替えている利用者がいるかは、devbase の側から確かめられない。いれば、ボリュームのグループが `env` ファイルの宣言(無ければ `default`)へ戻る。警告と CHANGELOG で知らせる | +| 警告の見え方 | CLI の標準エラーと TUI の画面で、警告が操作の出力に埋もれずに読めるか。リリース後テストで、検証用のプロジェクトに値を置いて `devbase ps` と `devbase list` から確かめる | +| 子プロセスの `devbase` | `up` が起動する子プロセスの `env init` は別の起動で、同じ置き場の警告をもう一度出しうる。子プロセスは共通の機密が無いときにだけ起動し、プロジェクトを持たないため、出るのは個人共通の置き場に値があるときに限られる | +| 確定仕様の更新 | `docs/specifications/secret-backend.md` の「重ね順」と、既知の課題として #185 を挙げた段落は、確定仕様化(`plan-to-spec`)で書き替える | diff --git a/issues/PLAN62_inject-account-group.md b/issues/PLAN62_inject-account-group.md new file mode 100644 index 00000000..9ca0974e --- /dev/null +++ b/issues/PLAN62_inject-account-group.md @@ -0,0 +1,108 @@ +# PLAN62: 機密の置き場に書いた `DEVBASE_ACCOUNT_GROUP` を注入しない + +対象 issue: devbasex/devbase#185 + +- ワークフローモード: `standard` + - 根拠: 機密の注入(`runtime.resolve` / `inject`)の本番の振る舞いを変える。`version: 1` の挙動も変わる + +## 依頼(原文) + +> `runtime.inject()` は機密の置き場(`.env` / age / OpenBao)の値をそのままプロセスの環境変数へ載せる。置き場に `DEVBASE_ACCOUNT_GROUP` が入っていると、`volume.manager.resolve_account_group()`(プロセスの環境変数を読む)が決めるボリュームのグループがその値に変わる。 +> +> - プロジェクトの `env` がグループを宣言していれば、ラッパーの `source` が後から上書きするので起きない +> - `$DEVBASE_ROOT/env` での宣言や、宣言が無いプロジェクトでは起きうる +> - `version: 1`(今の設定)でも同じ経路がある。PLAN56(#182 / #184)の `layout: group` では、機密の置き場のグループはファイルだけから決めるため、この場合は `up` / `scale` がボリュームとのグループの食い違いで止まる +> +> 注入の対象から `DEVBASE_ACCOUNT_GROUP` を外すかどうかは、`version: 1` の挙動を変えるため別に判断する。 + +## 目的 + +- アカウントグループ(ボリューム `devbase_home_` と、`layout: group` での機密の置き場)を決める値が、 + `env` ファイル(`projects//env` / `$DEVBASE_ROOT/env`)とシェルの環境変数からだけ来るようにする。 + 機密の置き場に書いた値が、どの設定(`version: 1` / `layout: group`)でもグループを変えないようにする + +## 前提 + +- 前提 1: **注入の対象から外す**(issue の「外すかどうか」への答え)。`version: 1` でも外す。 + 理由: 利用者向け文書(`docs/user/environment-variables.md` / `env-backend.md`)は PLAN56 以降 + 「置き場に書いた値はグループの決定に使われない」と説明しており、`version: 1` だけ置き場の値が効く今の + 挙動は文書と食い違っている。置き場の値でグループを切り替える使い方は文書に無い +- 前提 2: 外す場所は機密の合成(`runtime.resolve`)の 1 か所にする。`inject`・`child_env`・コンテナへ + 列挙する変数名は、どれも `resolve` の結果から作られるため、1 か所で全経路に効く +- 前提 3: 置き場に書かれた値を見つけたときは、無視したことを警告で 1 回知らせ、消し方 + (`devbase env delete DEVBASE_ACCOUNT_GROUP`、置き場に応じて `-p` / `--user` / `--group`)を示す。 + 値そのものは出さない +- 前提 4: `devbase env set DEVBASE_ACCOUNT_GROUP=...` は置き場へ書かずに拒否し、`env` ファイルへ書くよう案内する + (終了コード 1)。書いても使われない値を新たに作らないため。`env import` / `env edit` / 既存の値は拒否しない + (前提 3 の警告で知らせる) +- 前提 5: コンテナ内の `DEVBASE_ACCOUNT_GROUP` は今と同じく `volume/compose.py` が構成へ書く値 + (解決済みのグループ)で決まる。置き場の値がコンテナへ列挙されることは無くなる + +## 対象範囲 + +含む: + +- 機密の合成(`runtime.resolve`)から `DEVBASE_ACCOUNT_GROUP` を外すことと、その警告 +- `devbase env set` での拒否 +- 利用者向け文書(`docs/user/environment-variables.md` / `docs/user/env-backend.md` / `docs/user/cli-reference/03-env.md` の `env set`)の該当箇所と CHANGELOG + +含まない: + +- `DEVBASE_ACCOUNT_GROUP` 以外の「置き場に書くべきでない」キーの扱い(`DEVBASE_ROOT` など。必要なら別の issue) +- `env import` / `env edit` での拒否 +- 置き場に残っている値の自動削除 +- ボリュームのグループの決め方(`resolve_account_group`)と、`layout: group` の機密のグループの決め方の変更 +- 新しい型・永続データ・画面の追加(そのためクラス図・ER 図・画面遷移図を作らない)。要求に非機能の条件は無い(注入の要求の回数・経路を変えない) + +## 受け入れ条件 + +- [ ] 1. 前提: チーム共通の機密(`version: 1`・`plaintext` / `age` backend)に `DEVBASE_ACCOUNT_GROUP=kkg` があり、 + プロセスの環境変数に `DEVBASE_ACCOUNT_GROUP` が無い + 操作: `runtime.inject(root, project)` を呼ぶ + 結果: プロセスの環境変数に `DEVBASE_ACCOUNT_GROUP` が載らず、`resolve_account_group()` は `default` を返す +- [ ] 2. 前提: 1 と同じ置き場で、プロセスの環境変数に `DEVBASE_ACCOUNT_GROUP=with` がある(`env` ファイル由来) + 操作: `runtime.inject(root, project)` を呼ぶ + 結果: プロセスの環境変数は `with` のまま変わらない +- [ ] 3. 個人共通・プロジェクトのチーム・プロジェクトの個人の置き場にあっても、1・2 と同じになる +- [ ] 4. 置き場にあったときは、キー名と置き場の種類・消し方を含む警告が出て、値は出ない。 + 1 回の CLI 起動で同じ置き場について 2 回以上出ない +- [ ] 5. `resolve()` の結果(`SecretEnv.names` / `values`)に `DEVBASE_ACCOUNT_GROUP` が含まれない。 + したがって dev コンテナの `environment` へ置き場の値は列挙されない +- [ ] 6. `layout: group` で、置き場(`$DEVBASE_ROOT/env` にも `projects//env` にもグループの宣言が無い + プロジェクトの、共通の機密)に `DEVBASE_ACCOUNT_GROUP=kkg` があっても、`devbase up` の前の + グループの食い違いの検査で止まらない +- [ ] 7. `devbase env set DEVBASE_ACCOUNT_GROUP=kkg`(`-p` / `--user` / `--group` 付きも同じ)は、置き場へ書かずに + `env` ファイルへ書くよう案内して終了コード 1 +- [ ] 8. 他のキーの注入(重ね順・注入の履歴・`clear_injected` による巻き戻し)の既存テストが変更なしで通る +- [ ] 9. 全体テスト(`uv run pytest tests/`)が通る + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わる: `env set DEVBASE_ACCOUNT_GROUP=...` が拒否される。CHANGELOG に「変更」として書く | +| データ | 置き場の値は消さない | +| 既存の振る舞い | `version: 1` で置き場に `DEVBASE_ACCOUNT_GROUP` を書いていた利用者は、ボリュームのグループが `env` ファイルの宣言(無ければ `default`)へ戻る。警告で知らせる | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run pytest tests/ -q` | +| 静的解析 | `ruff check --select=E9,F63,F7,F82 lib` | +| 手動確認 | 検証用のプロジェクトで置き場に値を入れ、`devbase ps` 等で警告が出ること、`devbase env set` が拒否することを見る(リリース後テスト) | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | 注入の規則は `lib/devbase/env/runtime.py`、キー名は `lib/devbase/env/keys.py` | +| テスト戦略 | `resolve` / `inject` は `tests/env/` の単体テストで、差し替えの `SecretStore` を使う。`env set` は `tests/cli/` または `tests/env/` の既存の形に合わせる。`DEVBASE_ROOT` は tmp へ向ける | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 全体テスト | +| 確認してから行う | 前提 1(`version: 1` の挙動を変えること。設計 Pull Request の承認で確かめる) | +| 行わない | 置き場の値の自動削除、他のキーの扱いの変更 | From d41ea4d2478d3cf95d6c6e7f3e07ea410df82877 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Fri, 18 Sep 2026 21:22:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(PLAN62):=20env=20set=20=E3=81=AE?= =?UTF-8?q?=E6=8B=92=E5=90=A6=E3=81=A7=E8=B5=B7=E3=81=8D=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=82=E3=81=AE=E3=81=A8=20dispatch=20=E5=89=8D=E3=81=AE?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E3=81=AE=E8=AA=AD=E3=81=BF=E5=8F=96=E3=82=8A?= =?UTF-8?q?=E3=82=92=E5=88=86=E3=81=91=E3=81=A6=E6=9B=B8=E3=81=8D=E3=80=81?= =?UTF-8?q?=E5=89=8D=E6=8F=90=201=20=E3=81=AE=E5=87=BA=E5=85=B8=E3=82=92?= =?UTF-8?q?=E5=B0=8F=E7=AF=80=E5=90=8D=E3=81=BE=E3=81=A7=E7=A4=BA=E3=81=99?= =?UTF-8?q?=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- issues/PLAN62_inject-account-group-design.md | 6 ++++-- issues/PLAN62_inject-account-group.md | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/issues/PLAN62_inject-account-group-design.md b/issues/PLAN62_inject-account-group-design.md index e7faa2ac..cc091e19 100644 --- a/issues/PLAN62_inject-account-group-design.md +++ b/issues/PLAN62_inject-account-group-design.md @@ -108,7 +108,7 @@ graph TD | 名前 | `devbase env set KEY=VALUE [-p] [--user] [--group NAME]` | | 変わる入力 | `KEY` が前後の空白を除いて `DEVBASE_ACCOUNT_GROUP` と一致するとき | | 出力(拒否) | 終了コード 1。error ログ `DEVBASE_ACCOUNT_GROUP は機密の置き場へは書けません(置き場の値はアカウントグループの決定に使われません)。projects//env か $DEVBASE_ROOT/env に書いてください` | -| 置き場への作用 | 無し。`_open_target_env` より前に返すため、ファイルを作らず、サーバへ要求しない | +| 置き場への作用 | 拒否の処理は置き場を開かない。`_open_target_env` より前に返すため、書き込み・ファイルの作成・書き込みのための読み出し(`fresh`)をしない。dispatch 前の注入(`cli._load_secret_env`)による読み取りは、他のコマンドと同じく起きうる(`version: 1` では `env set` も注入の対象。`cli.py` は変えない) | | 引数の検査との順序 | `-p` / `--user` / `--group` の検査より前に拒否する。`--group` に使えない名前を渡しても終了コードは 1 | | 互換性 | 変わる。これまで書けたキーが書けなくなる。CHANGELOG の「変更」に書く。`env import` / `env edit` / 他のキーの `env set` は変わらない | @@ -212,7 +212,9 @@ TUI は 1 プロセスで操作を続けるため、同じ置き場の警告は ### 決定 7: `env set` は置き場を開く前に拒否する -`layout: group` の `--group` の検証や、サーバ backend での現物の読み出し(`fresh`)は、置き場を開く `_open_target_env` の中で行われる。その前に返せば、拒否する操作でサーバへの要求もファイルの作成も起きない。拒否の理由は宛先によらないため、引数の組み合わせの検査より先に置く。 +`layout: group` の `--group` の検証や、サーバ backend での現物の読み出し(`fresh`)は、置き場を開く `_open_target_env` の中で行われる。その前に返せば、拒否する操作で書き込みも、ファイルの作成も、書き込みのための読み出し(`fresh`)も起きない。 + +dispatch 前の注入による読み取りは残る。`env set` は `cli._NO_SECRET_INJECTION` に無く、`layout: group` のときだけ `_GROUPED_SELF_RESOLVING_ENV` で注入を飛ばす。そのため `version: 1` では、`cmd_env_set` に届く前に `runtime.inject` が置き場を読む。これは他のコマンドと同じ読み取りで、拒否の処理が起こすものではない。`env set` だけを注入の対象から外すと他のキーの `env set` の挙動も変わるため、`cli.py` は変えない。拒否の理由は宛先によらないため、引数の組み合わせの検査より先に置く。 `env import` と `env edit` は拒否しない(要求の前提 4)。どちらも複数のキーをまとめて扱い、1 キーのために全体を止めると他のキーの作業まで止まる。書かれた値は決定 3 の警告で知らせる。 diff --git a/issues/PLAN62_inject-account-group.md b/issues/PLAN62_inject-account-group.md index 9ca0974e..48ebdaf9 100644 --- a/issues/PLAN62_inject-account-group.md +++ b/issues/PLAN62_inject-account-group.md @@ -24,7 +24,8 @@ ## 前提 - 前提 1: **注入の対象から外す**(issue の「外すかどうか」への答え)。`version: 1` でも外す。 - 理由: 利用者向け文書(`docs/user/environment-variables.md` / `env-backend.md`)は PLAN56 以降 + 理由: 利用者向け文書(`docs/user/environment-variables.md` の「機密の置き場もグループで分ける(OpenBao)」と + `docs/user/env-backend.md` の「グループの決まり方」)は PLAN56 以降 「置き場に書いた値はグループの決定に使われない」と説明しており、`version: 1` だけ置き場の値が効く今の 挙動は文書と食い違っている。置き場の値でグループを切り替える使い方は文書に無い - 前提 2: 外す場所は機密の合成(`runtime.resolve`)の 1 か所にする。`inject`・`child_env`・コンテナへ