From 31a6613994693ce6f6b53743f8f3e3cb81bd7337 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:15:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(editor):=20=E3=83=8D=E3=82=B9=E3=83=88?= =?UTF-8?q?=20URI=20=E3=81=AE=E6=A1=88=E5=86=85=E3=81=AB=20DEVBASE=5FEDITO?= =?UTF-8?q?R=5FSSH=5FHOST=3D=20=E3=81=A7=E3=83=95=E3=83=A9=E3=83=83?= =?UTF-8?q?=E3=83=88=20URI=20=E3=81=B8=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88?= =?UTF-8?q?=E3=82=8B=E6=96=B9=E6=B3=95=E3=82=92=E6=B7=BB=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remote-SSH 統合ターミナルから up し、コンテナが ssh 先とも別のホスト (WSL2 等) に あるとき、案内に出るフラット URI を毎回手で貼らないと直接 attach できなかった。 空文字による自動検出のオプトアウト (resolve_editor_ssh_host) は実装済みだが、案内 にもドキュメントにも書かれておらず、利用者が辿り着けない。 - 案内の直後に `DEVBASE_EDITOR_SSH_HOST=` (空) で恒久化できることを示す - 環境変数ガイドに「手元の context で直接開く」節を足し、前提 (実行先で `docker --context ps` が通ること、executeInWSL のときは WSL 内に同名 context) を書く - 仕様書 (remote-docker-context) の attach URI 節へ空文字の意味を追記 - 空文字 + 解決済み context でフラット URI + settings.context で launch するテスト Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LeGvZnJ6w7NK7wWoVyN9H5 --- docs/specifications/remote-docker-context.md | 4 ++- docs/user/environment-variables.md | 15 +++++++-- lib/devbase/editor/opener.py | 7 +++++ tests/editor/test_opener.py | 33 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/docs/specifications/remote-docker-context.md b/docs/specifications/remote-docker-context.md index 722dadc4..bb0eaf46 100644 --- a/docs/specifications/remote-docker-context.md +++ b/docs/specifications/remote-docker-context.md @@ -119,7 +119,9 @@ root 所有か rootless Docker の可能性を警告して続行する。控え (ssh 先のときだけ)docker が実際に使う context(環境変数を外さない `docker context show`)」の 順で決める。解決した context があればローカル端末でもフラット URI に付ける。Remote-SSH 統合 端末でネスト URI と `settings.context` の両方が付くときは、手元の VS Code に同名の context が -あれば直接 attach できるフラット URI を info で提示する。 +あれば直接 attach できるフラット URI を info で提示し、`DEVBASE_EDITOR_SSH_HOST=`(空)で +恒久的にそのフラット URI へ切り替えられることも示す。空文字は自動検出のオプトアウトで、 +ネストを付けず `settings.context` だけを残す。 ```mermaid sequenceDiagram diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index 35be85d9..eda63b6b 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -277,7 +277,7 @@ DEVBASE_ACCOUNT_GROUP=kkg | `DEVBASE_EDITOR` | 起動コマンド(既定: `code`)。`cursor` / `code-insiders` 等も可 | | `DEVBASE_WORKSPACE` | 開く `*.code-workspace` ファイルの**コンテナ内絶対パス**を明示指定する(例 `/home/ubuntu/share/work/uttarov2-doc.workspace`)。**効くのはリポジトリ 1 件の構成だけ**です。2 件以上の構成では `devbase up` が自動生成した `/work/<プロジェクト名>.code-workspace` を直接開くため、この env を設定しても上書きできません。`~/share`(= 全コンテナ共有ボリューム `/persistent/ai/share` への symlink)配下に置けば全コンテナで共用可 | | `DEVBASE_OPEN_INDEX` | scale 時に開く dev インスタンス番号(既定: `1`) | -| `DEVBASE_EDITOR_SSH_HOST` | Remote-SSH 跨ホスト構成での ssh-remote ホスト名(例 `mac2`)。**通常は `~/.vscode-server` から自動検出**され不要。検出が外れる場合のみ明示。下記「リモート Docker」参照 | +| `DEVBASE_EDITOR_SSH_HOST` | Remote-SSH 跨ホスト構成での ssh-remote ホスト名(例 `mac2`)。**通常は `~/.vscode-server` から自動検出**され不要。検出が外れる場合のみ明示。**空文字(`DEVBASE_EDITOR_SSH_HOST=`)はネストのオプトアウト**で、フラット URI を強制する。下記「リモート Docker」参照 | | `DEVBASE_EDITOR_DOCKER_CONTEXT` | attach に使う docker context を手で決めたいときだけ明示する。未設定なら devbase が解決した context(`--context` / `DEVBASE_DOCKER_CONTEXT` / `project.local.yml`)、それも無ければ跨ホスト時にホストの `docker context show` | | `DEVBASE_DOCKER_CONTEXT` | `devbase up/down/ps/logs/login/scale/build/rebuild` が向ける docker context。`project.local.yml` の `docker.context` より優先し、CLI `--context` に負ける。グローバル `.env` に書くと全プロジェクトが同じホストへ向くため、通常は `project.local.yml` に書く。下記「リモート Docker」参照 | | `DEVBASE_WINDOW_TITLE` | attach 先 VS Code の `window.title` テンプレート。`{container}` が実コンテナ名(例 `nyle-dx-dev-1`)に置換される。既定は `{container}${separator}${dirty}${activeEditorShort}`。`0` / `false` / `off` / 空文字で無効化。下記「ウィンドウタイトル」参照 | @@ -404,7 +404,7 @@ gid が変わったらファイルを消すか `docker.gid` を書いてくだ | ローカル端末(Mac / Linux / WSL) | 同じマシン(従来) | フラット URI | | ローカル端末 | リモート context | フラット URI + `settings.context=`。手元の Dev Containers 拡張がその context 経由で attach する | | Remote-SSH 統合ターミナル(Windows VS Code → Mac) | Mac | ネスト URI `…@ssh-remote+`(下記) | -| Remote-SSH 統合ターミナル | リモート context(WSL / EC2 など) | ネスト URI + `settings.context=`。Mac の Dev Containers が context 経由で attach する。あわせて、手元の VS Code に同名の context があれば直接 attach できるフラット URI も表示する(Windows → Mac → WSL(Windows) の一周を避けたいとき) | +| Remote-SSH 統合ターミナル | リモート context(WSL / EC2 など) | ネスト URI + `settings.context=`。Mac の Dev Containers が context 経由で attach する。あわせて、手元の VS Code に同名の context があれば直接 attach できるフラット URI も表示する(Windows → Mac → WSL(Windows) の一周を避けたいとき)。毎回そのフラット URI で開きたければ `DEVBASE_EDITOR_SSH_HOST=`(空)を書く(下記「手元の context で直接開く」) | `settings.context` は「`DEVBASE_EDITOR_DOCKER_CONTEXT` の明示 → devbase が解決した context → (ssh 先のときだけ)`docker context show`」の順で決まります。 @@ -430,6 +430,17 @@ DEVBASE_EDITOR_SSH_HOST=mac2 > 同一ホスト構成(手元 Mac/Linux で直接、または ssh 先の Docker にコンテナが無い場合)では ssh-remote ホストは付かず、従来どおりフラット URI で開きます。 +##### 手元の context で直接開く(ネストのオプトアウト) + +コンテナが ssh 先とも別のホスト(例 WSL2)にあり、**手元の VS Code 側にも同名の docker context がある**なら、ssh 先を経由するネスト URI ではなく、手元の Dev Containers 拡張にその context で直接 attach させられます。`DEVBASE_EDITOR_SSH_HOST` を**空文字で明示**すると自動検出を行わず、`settings.context` 付きのフラット URI で `code` を起動します: + +```sh +# projects//env(その context を使うプロジェクトだけに効かせる) +DEVBASE_EDITOR_SSH_HOST= +``` + +前提は、`code` の実行先(手元の VS Code が docker CLI を呼ぶ側)で `docker --context ps` がコンテナを返すことです。Windows の VS Code で `dev.containers.executeInWSL` が `true` なら WSL 内の CLI が使われるので、WSL 内に `docker context create --docker host=unix:///var/run/docker.sock` で同名 context を作れば足ります。行を消せば自動検出(ネスト URI)に戻ります。 + ##### 制約 - `docker context use` で**現在の context 自体**をリモートへ向けた状態は、これまでどおりの diff --git a/lib/devbase/editor/opener.py b/lib/devbase/editor/opener.py index 70d02406..d1717097 100644 --- a/lib/devbase/editor/opener.py +++ b/lib/devbase/editor/opener.py @@ -669,6 +669,8 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, ネスト URI (ssh_host + docker_context) のときは、手元 VS Code に同名 context が あれば ssh 先を経由せず直接 attach できるフラット URI を info ログで提示する。 + あわせて、``DEVBASE_EDITOR_SSH_HOST=`` (空) で恒久的にフラット URI へ切り替えられる + ことも示す (案内だけでは毎回手で貼ることになる)。 戻り値は ``(uri, uri_flag)``。 """ @@ -694,6 +696,11 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, "次で直接 attach できます:", docker_context) logger.info(" %s %s '%s'", " ".join(shlex.quote(c) for c in display), uri_flag, flat) + # 空文字の明示はネストのオプトアウト (resolve_editor_ssh_host)。毎回手で貼る + # 代わりに恒久化する方法を、その場で示す。 + logger.info( + " env に DEVBASE_EDITOR_SSH_HOST= (空) を書くと、次回からこのフラット URI で" + "直接開きます") return uri, uri_flag diff --git a/tests/editor/test_opener.py b/tests/editor/test_opener.py index b7cd0e6f..674111b0 100644 --- a/tests/editor/test_opener.py +++ b/tests/editor/test_opener.py @@ -967,6 +967,39 @@ def boom(*a, **kw): text = "\n".join(r.getMessage() for r in caplog.records) flat = uri.replace("@ssh-remote+mac2", "") assert flat in text and "同名" in text + # 恒久化の方法 (空文字オプトアウト) もその場で示す。 + assert "DEVBASE_EDITOR_SSH_HOST=" in text + + +def test_open_editor_remote_ssh_empty_ssh_host_launches_flat_uri_with_context(monkeypatch, caplog): + """Remote-SSH + 解決した context でも DEVBASE_EDITOR_SSH_HOST= (空) ならフラット URI で launch。 + + 手元の VS Code に同名 context がある構成 (Windows → WSL の docker を直接) で、案内の + フラット URI を毎回手で貼らずに済ませる経路。settings.context は残す。 + """ + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + + def boom(*a, **kw): + raise AssertionError("docker context show should not run") + + monkeypatch.setattr(opener.subprocess, "run", boom) + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="investment", dev_service_name="dev", workdir="/work/investment", + environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_SSH_HOST": ""}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + docker_context="wsl", + ) + uri = calls[0][2] + assert "@ssh-remote+" not in uri and uri.endswith("/work/investment") + assert _decode(uri)["settings"]["context"] == "wsl" + text = "\n".join(r.getMessage() for r in caplog.records) + assert "同名" not in text # ネストしていないので案内は出ない def test_open_editor_explicit_editor_context_beats_resolved(monkeypatch): From c759338d507abc3248282dc1e823873a43c5b3a2 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:39:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(editor):=20=E6=81=92=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E3=81=AE=E6=A1=88=E5=86=85=E3=82=92=20context=20=E3=81=8C?= =?UTF-8?q?=E6=98=8E=E7=A4=BA=E3=81=8B=E8=A7=A3=E6=B1=BA=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E3=81=AE=E3=81=A8=E3=81=8D=E3=81=AB=E9=99=90=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docker context show` の推測だけから settings.context が付いている構成で案内どおり DEVBASE_EDITOR_SSH_HOST= (空) を書くと、ssh_host が外れて推測も行われず settings.context が消え、提示したフラット URI と違う daemon へ繋ぎに行く (cross-review codex 指摘)。 - 案内は resolved (devbase の解決結果) か DEVBASE_EDITOR_DOCKER_CONTEXT の明示が あるときだけ出す - DEVBASE_EDITOR_DOCKER_CONTEXT の明示は ssh の有無によらず settings.context に 載せる (推測だけを ssh 先に限る) - ドキュメントに前提 (context の出所) を明記 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LeGvZnJ6w7NK7wWoVyN9H5 --- docs/specifications/remote-docker-context.md | 4 +- docs/user/environment-variables.md | 2 +- lib/devbase/editor/opener.py | 24 ++++++--- tests/editor/test_opener.py | 56 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/docs/specifications/remote-docker-context.md b/docs/specifications/remote-docker-context.md index bb0eaf46..d0f6a1e1 100644 --- a/docs/specifications/remote-docker-context.md +++ b/docs/specifications/remote-docker-context.md @@ -121,7 +121,9 @@ root 所有か rootless Docker の可能性を警告して続行する。控え 端末でネスト URI と `settings.context` の両方が付くときは、手元の VS Code に同名の context が あれば直接 attach できるフラット URI を info で提示し、`DEVBASE_EDITOR_SSH_HOST=`(空)で 恒久的にそのフラット URI へ切り替えられることも示す。空文字は自動検出のオプトアウトで、 -ネストを付けず `settings.context` だけを残す。 +ネストを付けず `settings.context` だけを残す。明示と devbase の解決結果は ssh の有無に +よらず付けるが、`docker context show` の推測は ssh 先のときに限るため、推測だけが元の +ときは空文字で `settings.context` も消える。その場合は恒久化の案内を出さない。 ```mermaid sequenceDiagram diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index eda63b6b..378265cd 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -439,7 +439,7 @@ DEVBASE_EDITOR_SSH_HOST=mac2 DEVBASE_EDITOR_SSH_HOST= ``` -前提は、`code` の実行先(手元の VS Code が docker CLI を呼ぶ側)で `docker --context ps` がコンテナを返すことです。Windows の VS Code で `dev.containers.executeInWSL` が `true` なら WSL 内の CLI が使われるので、WSL 内に `docker context create --docker host=unix:///var/run/docker.sock` で同名 context を作れば足ります。行を消せば自動検出(ネスト URI)に戻ります。 +前提は 2 つです。1 つは、`code` の実行先(手元の VS Code が docker CLI を呼ぶ側)で `docker --context ps` がコンテナを返すこと。もう 1 つは、`settings.context` が **devbase の解決した context(`project.local.yml` / `--context` / `DEVBASE_DOCKER_CONTEXT`)か `DEVBASE_EDITOR_DOCKER_CONTEXT` の明示**から来ていることです。ssh 先の `docker context show` の推測だけに頼っている構成では、空文字にすると推測も行われず `settings.context` が付かない(手元の既定 daemon へ繋ぎに行く)ため、`devbase up` はこの案内を出しません。Windows の VS Code で `dev.containers.executeInWSL` が `true` なら WSL 内の CLI が使われるので、WSL 内に `docker context create --docker host=unix:///var/run/docker.sock` で同名 context を作れば足ります。行を消せば自動検出(ネスト URI)に戻ります。 ##### 制約 diff --git a/lib/devbase/editor/opener.py b/lib/devbase/editor/opener.py index d1717097..78f5af63 100644 --- a/lib/devbase/editor/opener.py +++ b/lib/devbase/editor/opener.py @@ -670,14 +670,21 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, ネスト URI (ssh_host + docker_context) のときは、手元 VS Code に同名 context が あれば ssh 先を経由せず直接 attach できるフラット URI を info ログで提示する。 あわせて、``DEVBASE_EDITOR_SSH_HOST=`` (空) で恒久的にフラット URI へ切り替えられる - ことも示す (案内だけでは毎回手で貼ることになる)。 + ことも示す (案内だけでは毎回手で貼ることになる)。この案内は context が明示か devbase の + 解決結果から来ているときだけ出す。``docker context show`` の推測だけが元のときは、 + ssh_host を外すと推測も行われず ``settings.context`` が消えるため出さない。 戻り値は ``(uri, uri_flag)``。 """ ssh_host = (resolve_editor_ssh_host(env, auto_detect=ctx.in_vscode) if ctx.is_ssh else None) - if ssh_host or docker_context: - docker_context = resolve_docker_context(env, default=docker_context) + # 明示 (DEVBASE_EDITOR_DOCKER_CONTEXT) と devbase の解決結果は ssh の有無によらず + # 付ける。`docker context show` の推測だけは ssh 先のときに限る (ローカル端末で + # 毎回 docker を叩かない)。 + explicit_context = env.get("DEVBASE_EDITOR_DOCKER_CONTEXT") + resolved_context = docker_context + if ssh_host or resolved_context or explicit_context is not None: + docker_context = resolve_docker_context(env, default=resolved_context) else: docker_context = None # DEVBASE_WORKSPACE があれば *.code-workspace をワークスペースとして開く。VS Code は @@ -697,10 +704,13 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, logger.info(" %s %s '%s'", " ".join(shlex.quote(c) for c in display), uri_flag, flat) # 空文字の明示はネストのオプトアウト (resolve_editor_ssh_host)。毎回手で貼る - # 代わりに恒久化する方法を、その場で示す。 - logger.info( - " env に DEVBASE_EDITOR_SSH_HOST= (空) を書くと、次回からこのフラット URI で" - "直接開きます") + # 代わりに恒久化する方法を、その場で示す。ただし context が `docker context + # show` の推測だけから来ている場合は、ssh_host を外すと推測も行われず + # settings.context が消える (別の daemon へ繋ぎに行く) ため案内しない。 + if resolved_context or explicit_context is not None: + logger.info( + " env に DEVBASE_EDITOR_SSH_HOST= (空) を書くと、次回からこのフラット URI で" + "直接開きます") return uri, uri_flag diff --git a/tests/editor/test_opener.py b/tests/editor/test_opener.py index 674111b0..9b0bfe18 100644 --- a/tests/editor/test_opener.py +++ b/tests/editor/test_opener.py @@ -1002,6 +1002,62 @@ def boom(*a, **kw): assert "同名" not in text # ネストしていないので案内は出ない +def test_open_editor_remote_ssh_context_from_docker_show_omits_opt_out_hint(monkeypatch, caplog): + """context が `docker context show` の推測だけのときは、空文字オプトアウトの案内を出さない。 + + ssh_host を外すと推測も行われず settings.context が消える (別の daemon へ繋ぐ) ため、 + 案内どおりにすると提示したフラット URI と違うものが開く。 + """ + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + monkeypatch.setattr(opener, "resolve_docker_context", + lambda env, default=None, **kw: default or "probed") + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_SSH_HOST": "mac2"}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + uri = calls[0][2] + assert "@ssh-remote+mac2" in uri and _decode(uri)["settings"]["context"] == "probed" + text = "\n".join(r.getMessage() for r in caplog.records) + assert "同名" in text and "DEVBASE_EDITOR_SSH_HOST=" not in text + + +def test_open_editor_remote_ssh_explicit_editor_context_survives_opt_out(monkeypatch, caplog): + """DEVBASE_EDITOR_DOCKER_CONTEXT の明示は、空文字オプトアウト後も settings.context に残る。""" + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + + def boom(*a, **kw): + raise AssertionError("docker context show should not run") + + monkeypatch.setattr(opener.subprocess, "run", boom) + base = {"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_DOCKER_CONTEXT": "wsl"} + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={**base, "DEVBASE_EDITOR_SSH_HOST": "mac2"}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + assert "DEVBASE_EDITOR_SSH_HOST=" in "\n".join(r.getMessage() for r in caplog.records) + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={**base, "DEVBASE_EDITOR_SSH_HOST": ""}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + flat = calls[1][2] + assert "@ssh-remote" not in flat and _decode(flat)["settings"]["context"] == "wsl" + + def test_open_editor_explicit_editor_context_beats_resolved(monkeypatch): monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") calls = []