From e9f3acb27145ce815609fcd22f2f9344a031c402 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Tue, 7 Jul 2026 10:40:40 -0500 Subject: [PATCH 1/7] refactor(devcontainer, scripts): split headroom proxy relays and migrate Kilo auth providers The headroom proxy was split into two independent SSH-tunnelled ports (azure :9797, ow :9798) to support optimized and passthrough backends. Update devcontainer relay logic to handle both ports separately via socat relays on host.docker.internal:. Kilo auth.json provider keys were updated from the single fnal-litellm to fnal-azure and fnal-ow (sharing the same upstream gateway token). git-ai-commit now tries providers in order (fnal-azure, fnal-ow, fnal-litellm) when GIT_AI_COMMIT_KILO_PROVIDER is unset, ensuring backward compatibility during migration. Documentation and tests were updated to reflect the new provider fallback behavior and relay architecture. --- .devcontainer/devcontainer.json | 7 +-- .devcontainer/ensure-repos.sh | 76 ++++++++++++++++++------------ .devcontainer/post-create.sh | 9 +++- scripts/git-ai-commit | 44 +++++++++++------ scripts/man/man1/git-ai-commit.1 | 10 ++-- scripts/test/test_git_ai_commit.py | 42 ++++++++++++++++- 6 files changed, 135 insertions(+), 53 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c60f4acb5..2f715e037 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,9 +7,10 @@ "workspaceFolder": "/workspaces/phlex", "remoteUser": "root", "remoteEnv": { - // KILO_CONFIG_CONTENT_DOCKER rewrites 127.0.0.1:PORT to - // host.docker.internal:RELAY_PORT for the socat relay used when - // headroom runs as a local proxy. Passed through here so that + // KILO_CONFIG_CONTENT_DOCKER rewrites both 127.0.0.1 headroom ports + // (azure :9797, ow :9798) to their host.docker.internal:RELAY_PORT + // (port+10000) for the socat relays used when headroom runs as a local + // split proxy pair. Passed through here so that // post-create.sh can wire it into the container's .bashrc // conditionally: if non-empty it sets KILO_CONFIG_CONTENT to this // value; otherwise KILO_CONFIG_CONTENT is left unset and Kilo falls diff --git a/.devcontainer/ensure-repos.sh b/.devcontainer/ensure-repos.sh index 9303c9ddf..38a599f59 100755 --- a/.devcontainer/ensure-repos.sh +++ b/.devcontainer/ensure-repos.sh @@ -150,38 +150,54 @@ s.bind('${PROXY_SOCKET}') " 2>/dev/null || touch "${PROXY_SOCKET}" fi -# --- Headroom Proxy Relay for Devcontainer --- +# --- Headroom Proxy Relays for Devcontainer --- # -# The headroom proxy is an SSH-tunnelled port bound only to 127.0.0.1 on this -# host. Rootless Podman uses pasta for container networking, so containers -# reach the host via host.docker.internal (169.254.1.2) rather than via a -# bridge interface. However, pasta maps host.docker.internal to the host's -# loopback only for ports that are actually listening on all interfaces -- -# headroom's port is bound to 127.0.0.1 only and is therefore unreachable. +# The headroom proxy was split into TWO SSH-tunnelled ports, each bound only to +# 127.0.0.1 on this host: +# HEADROOM_AZURE_PORT (9797) : optimized proxy, Kilo provider fnal-azure +# HEADROOM_OW_PORT (9798) : passthrough proxy, Kilo provider fnal-ow +# Rootless Podman uses pasta for container networking, so containers reach the +# host via host.docker.internal (169.254.1.2) rather than via a bridge +# interface. However, pasta maps host.docker.internal to the host's loopback +# only for ports that are actually listening on all interfaces -- headroom's +# ports are bound to 127.0.0.1 only and are therefore unreachable. # -# We relay headroom onto a different port on 0.0.0.0 so that containers can -# reach it via host.docker.internal:$HEADROOM_RELAY_PORT. A different port is -# required because 0.0.0.0:$HEADROOM_PORT would conflict with the existing -# 127.0.0.1:$HEADROOM_PORT listener. KILO_CONFIG_CONTENT_DOCKER is passed into the -# devcontainer and post-create.sh wires it into /root/.bashrc to point at -# host.docker.internal:$HEADROOM_RELAY_PORT. - -HEADROOM_PORT="${HEADROOM_PORT:-9797}" -HEADROOM_RELAY_PORT=$(( HEADROOM_PORT + 10000 )) -HEADROOM_LOCAL="127.0.0.1:${HEADROOM_PORT}" - -if ss -tlnp 2>/dev/null | grep -q "127.0.0.1:${HEADROOM_PORT}"; then - start_socat_relay \ - "Headroom proxy" \ - "socat TCP-LISTEN:${HEADROOM_RELAY_PORT}" \ - "TCP-LISTEN:${HEADROOM_RELAY_PORT},fork,reuseaddr" \ - "TCP:${HEADROOM_LOCAL}" \ - "/tmp/socat-headroom.log" \ - "ss -tlnp 2>/dev/null | grep -q ':${HEADROOM_RELAY_PORT} '" || true -else - echo "WARNING: headroom proxy not detected at ${HEADROOM_LOCAL}; skipping relay" >&2 - echo " Ensure the SSH tunnel is active (headroom running on your laptop)" >&2 -fi +# We relay EACH headroom port onto a different port on 0.0.0.0 so that +# containers can reach them via host.docker.internal:. A different +# port is required because 0.0.0.0: would conflict with the existing +# 127.0.0.1: listener. KILO_CONFIG_CONTENT_DOCKER (built in the shell rc) +# rewrites both loopback ports to their host.docker.internal relay ports and is +# wired into /root/.bashrc by post-create.sh. + +HEADROOM_AZURE_PORT="${HEADROOM_AZURE_PORT:-9797}" +HEADROOM_OW_PORT="${HEADROOM_OW_PORT:-9798}" + +# relay_headroom_port ROLE PROXY_PORT +# Relay 127.0.0.1:PROXY_PORT to 0.0.0.0:(PROXY_PORT+10000) if the proxy is +# listening; otherwise warn and skip. Each role gets an independent relay +# and log file so the two never collide. +relay_headroom_port() { + local role="$1" + local proxy_port="$2" + local relay_port=$(( proxy_port + 10000 )) + local local_addr="127.0.0.1:${proxy_port}" + + if ss -tlnp 2>/dev/null | grep -q "127.0.0.1:${proxy_port}"; then + start_socat_relay \ + "Headroom ${role} proxy" \ + "socat TCP-LISTEN:${relay_port}" \ + "TCP-LISTEN:${relay_port},fork,reuseaddr" \ + "TCP:${local_addr}" \ + "/tmp/socat-headroom-${role}.log" \ + "ss -tlnp 2>/dev/null | grep -q ':${relay_port} '" || true + else + echo "WARNING: headroom ${role} proxy not detected at ${local_addr}; skipping relay" >&2 + echo " Ensure the SSH tunnel is active (headroom-${role} running on your laptop)" >&2 + fi +} + +relay_headroom_port azure "${HEADROOM_AZURE_PORT}" +relay_headroom_port ow "${HEADROOM_OW_PORT}" # Ensure remaining source bind mount points exist. ensure_bind_dir "$HOME/.aws" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index f7f35673f..f58b197b9 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -39,10 +39,17 @@ import json import os from pathlib import Path +# The single fnal-litellm provider was split into fnal-azure (optimized) and +# fnal-ow (passthrough); both share the same upstream gateway key. Seed both +# provider keys so Kilo resolves whichever provider a model is routed through. +key = os.environ["KILO_API_KEY"] p = Path("/root/.local/share/kilo/auth.json") p.write_text( json.dumps( - {"fnal-litellm": {"type": "api", "key": os.environ["KILO_API_KEY"]}}, + { + "fnal-azure": {"type": "api", "key": key}, + "fnal-ow": {"type": "api", "key": key}, + }, indent=2, ) + "\n", diff --git a/scripts/git-ai-commit b/scripts/git-ai-commit index 99cea172a..a07dfb653 100755 --- a/scripts/git-ai-commit +++ b/scripts/git-ai-commit @@ -77,11 +77,12 @@ Environment variables GIT_AI_COMMIT_KILO_API kilo backend base URL (default: https://litellm.fnal.gov/v1) GIT_AI_COMMIT_KILO_MODEL kilo backend default model (default: azure/claude-haiku-4-5); setting this env var suppresses auto-escalation - GIT_AI_COMMIT_KILO_PROVIDER kilo auth.json provider key (default: fnal-litellm) + GIT_AI_COMMIT_KILO_PROVIDER kilo auth.json provider key (default: tries + fnal-azure, then fnal-ow, then legacy fnal-litellm) Token resolution (kilo backend): 1. GIT_AI_COMMIT_TOKEN - 2. ~/.local/share/kilo/auth.json (fnal-litellm key) + 2. ~/.local/share/kilo/auth.json (fnal-azure / fnal-ow key) 3. GITHUB_TOKEN / GH_TOKEN 4. `gh auth token` 5. ~/.config/gh/hosts.yml @@ -128,7 +129,18 @@ _DEFAULT_MODEL_KILO = _KILO_MODEL_ENV or "azure/claude-haiku-4-5" # True when the user has explicitly pinned a kilo model via the environment, # which suppresses auto-escalation even if the diff is large. _KILO_MODEL_PINNED_BY_ENV = bool(_KILO_MODEL_ENV) -_DEFAULT_KILO_PROVIDER = os.environ.get("GIT_AI_COMMIT_KILO_PROVIDER", "fnal-litellm") +# auth.json provider key(s) that hold the FNAL gateway API token. The former +# single "fnal-litellm" provider was split into "fnal-azure" and "fnal-ow", +# which share the same upstream key; "fnal-litellm" is retired. When the user +# does not pin GIT_AI_COMMIT_KILO_PROVIDER, try the new keys in order (and the +# retired name last, for any un-migrated auth.json). git-ai-commit talks to +# the LiteLLM gateway directly, so the provider key only selects the auth +# token — model names remain bare (e.g. "azure/...", "qwen/..."). +_KILO_PROVIDER_ENV = os.environ.get("GIT_AI_COMMIT_KILO_PROVIDER", "").strip() +_DEFAULT_KILO_PROVIDER = _KILO_PROVIDER_ENV or "fnal-azure" +_KILO_PROVIDER_FALLBACKS = ( + [_KILO_PROVIDER_ENV] if _KILO_PROVIDER_ENV else ["fnal-azure", "fnal-ow", "fnal-litellm"] +) _DEFAULT_MODEL_OTHER = "gpt-4o" _MAX_DIFF_CHARS = 60_000 @@ -372,8 +384,11 @@ def _get_instructions(root: Path) -> str: def _kilo_auth_token() -> str | None: """Read the kilo provider API key from kilo's auth.json, if present. - The provider name is taken from GIT_AI_COMMIT_KILO_PROVIDER (default: - "fnal-litellm"), matching the key used in ~/.config/kilo/kilo.jsonc. + The provider name(s) come from GIT_AI_COMMIT_KILO_PROVIDER when set; + otherwise the split FNAL providers are tried in order + ("fnal-azure", "fnal-ow", then the retired "fnal-litellm"). These share + the same upstream gateway token, so the first present key wins. The names + match the provider keys in ~/.config/kilo/kilo.jsonc. """ if not _KILO_AUTH_JSON.exists(): return None @@ -383,13 +398,14 @@ def _kilo_auth_token() -> str | None: return None if not isinstance(data, dict): return None - provider = data.get(_DEFAULT_KILO_PROVIDER) - if not isinstance(provider, dict): - return None - key = provider.get("key", "") - if not isinstance(key, str): - return None - return key.strip() or None + for provider_name in _KILO_PROVIDER_FALLBACKS: + provider = data.get(provider_name) + if not isinstance(provider, dict): + continue + key = provider.get("key", "") + if isinstance(key, str) and key.strip(): + return key.strip() + return None def _gh_config_token() -> str | None: @@ -462,8 +478,8 @@ def _token(backend: str) -> str: if backend == "kilo": raise _Error( "No FNAL LiteLLM token found. " - "Ensure kilo is configured with the fnal-litellm provider, or " - "set GIT_AI_COMMIT_TOKEN." + "Ensure kilo is configured with the fnal-azure or fnal-ow " + "provider, or set GIT_AI_COMMIT_TOKEN." ) raise _Error( "No API token found. Set GIT_AI_COMMIT_TOKEN, GITHUB_TOKEN, or run 'gh auth login'." diff --git a/scripts/man/man1/git-ai-commit.1 b/scripts/man/man1/git-ai-commit.1 index 0600e6a13..2d6793a0c 100644 --- a/scripts/man/man1/git-ai-commit.1 +++ b/scripts/man/man1/git-ai-commit.1 @@ -136,7 +136,7 @@ Token resolution order: .IP "1." 4 \fBGIT_AI_COMMIT_TOKEN\fR .IP "2." 4 -\fB~/.local/share/kilo/auth.json\fR (\fIfnal\-litellm\fR key) +\fB~/.local/share/kilo/auth.json\fR (\fIfnal\-azure\fR / \fIfnal\-ow\fR key) .IP "3." 4 \fBGITHUB_TOKEN\fR / \fBGH_TOKEN\fR .IP "4." 4 @@ -197,7 +197,10 @@ Default model for the \fBkilo\fR backend. Default: \fBazure/claude\-haiku\-4\-5 Setting this variable suppresses auto\-escalation even for large prompts. .TP \fBGIT_AI_COMMIT_KILO_PROVIDER\fR -Provider key in \fB~/.local/share/kilo/auth.json\fR. Default: \fBfnal\-litellm\fR. +Provider key in \fB~/.local/share/kilo/auth.json\fR. When unset, the split +FNAL providers are tried in order: \fBfnal\-azure\fR, then \fBfnal\-ow\fR, then +the retired \fBfnal\-litellm\fR. These share the same upstream gateway token, +so the first key present is used. .TP \fBEDITOR\fR, \fBVISUAL\fR Editor invoked by \fB\-\-edit\fR. Checked in order; \fBvi\fR is the final fallback. @@ -246,7 +249,8 @@ echo "Closes #1234; keep message terse" | git ai\-commit \-\-yes .SH FILES .TP \fB~/.local/share/kilo/auth.json\fR -Kilo credentials file; read for the \fBfnal\-litellm\fR provider key. +Kilo credentials file; read for the \fBfnal\-azure\fR / \fBfnal\-ow\fR provider +key (legacy \fBfnal\-litellm\fR still honored if present). .TP \fB~/.config/git\-ai\-commit/instructions.md\fR User\-level instructions included in every prompt. diff --git a/scripts/test/test_git_ai_commit.py b/scripts/test/test_git_ai_commit.py index cbce95ee0..a527a1700 100644 --- a/scripts/test/test_git_ai_commit.py +++ b/scripts/test/test_git_ai_commit.py @@ -67,7 +67,7 @@ class TestKiloAuthToken: """_kilo_auth_token validates the kilo credentials file before reading.""" - _PROVIDER = "fnal-litellm" + _PROVIDER = "fnal-azure" def _write(self, path: Path, obj: object) -> None: path.parent.mkdir(parents=True, exist_ok=True) @@ -75,7 +75,9 @@ def _write(self, path: Path, obj: object) -> None: def _setup(self, monkeypatch: pytest.MonkeyPatch, path: Path) -> None: monkeypatch.setattr(_M, "_KILO_AUTH_JSON", path) - monkeypatch.setattr(_M, "_DEFAULT_KILO_PROVIDER", self._PROVIDER) + # _kilo_auth_token iterates _KILO_PROVIDER_FALLBACKS; pin it to a single + # provider for the single-provider test cases below. + monkeypatch.setattr(_M, "_KILO_PROVIDER_FALLBACKS", [self._PROVIDER]) def test_file_missing_returns_none( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -173,6 +175,42 @@ def test_valid_key_no_whitespace( self._setup(monkeypatch, p) assert _kilo_auth_token() == "tok123" + def test_provider_fallback_order( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """First present provider in the fallback list wins. + + The retired fnal-litellm provider was split into fnal-azure and + fnal-ow; _kilo_auth_token tries the list in order and returns the first + provider that has a usable key, skipping absent earlier entries. + """ + p = tmp_path / "auth.json" + # fnal-azure absent; fnal-ow present -> fnal-ow key is used. + self._write(p, {"fnal-ow": {"key": "ow-token"}}) + monkeypatch.setattr(_M, "_KILO_AUTH_JSON", p) + monkeypatch.setattr( + _M, "_KILO_PROVIDER_FALLBACKS", ["fnal-azure", "fnal-ow", "fnal-litellm"] + ) + assert _kilo_auth_token() == "ow-token" + + def test_provider_fallback_prefers_earlier( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """When multiple providers are present, the earliest in the list wins.""" + p = tmp_path / "auth.json" + self._write( + p, + { + "fnal-azure": {"key": "azure-token"}, + "fnal-ow": {"key": "ow-token"}, + }, + ) + monkeypatch.setattr(_M, "_KILO_AUTH_JSON", p) + monkeypatch.setattr( + _M, "_KILO_PROVIDER_FALLBACKS", ["fnal-azure", "fnal-ow", "fnal-litellm"] + ) + assert _kilo_auth_token() == "azure-token" + # =========================================================================== # _gh_cli_token From 19cc0da00915bd1ff80c719cc3204285814073eb Mon Sep 17 00:00:00 2001 From: Chris Green Date: Tue, 30 Jun 2026 08:36:01 -0500 Subject: [PATCH 2/7] Migrate devcontainer to Docker Compose and update toolchain Switch `devcontainer.json` to use `docker-compose.yml` for service definition, volume mounts, and environment variables to improve configurability and consistency. Key changes include: - Update `Dockerfile` to use a pinned base image (`2026-06-24`), install additional utilities (`jq`, `tree`, `yq`), and upgrade Node.js to 26.x. - Replace `kiro-cli` with `@kilocode/cli` installed via npm. - Refactor `KILO_CONFIG_CONTENT` handling: `ensure-repos.sh` now generates a `.phlex-kilo.env` file on the host which is loaded by Docker Compose. This ensures the VS Code plugin receives the correct configuration (including the `host.docker.internal` relay) at startup, replacing the previous `.bashrc` modification in `post-create.sh`. - Remove Copilot-related extensions and settings from the workspace and devcontainer configuration. --- .devcontainer/Dockerfile | 21 +++++++++---- .devcontainer/codespace.code-workspace | 1 - .devcontainer/devcontainer.json | 41 ++------------------------ .devcontainer/docker-compose.yml | 34 +++++++++++++++++++++ .devcontainer/post-create.sh | 12 -------- 5 files changed, 53 insertions(+), 56 deletions(-) create mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6e4a9add9..68a72a493 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/framework-r-d/phlex-dev:latest +FROM ghcr.io/framework-r-d/phlex-dev:2026-06-24 # Validate Python site-packages symlink. # Invoke bash explicitly: the script uses bash features (pipefail, arrays, @@ -88,15 +88,26 @@ if [ "$(dpkg-divert --truename /usr/bin/man)" = "/usr/bin/man.REAL" ]; then fi INSTALL_MAN_TOOLS -# Install podman client and socat inside the container to support nested +# Install dev utils # container communication and provide a 'docker' command alias. -RUN bash <<'INSTALL_PODMAN_CLIENT' +RUN bash <<'INSTALL_DEV_UTILS' set -euo pipefail apt-get update -apt-get install -y --no-install-recommends ssh podman socat +apt-get install -y --no-install-recommends jq podman ssh socat tree yq +apt purge nodejs npm -y && apt autoremove -y || true +sudo apt-get install -y curl +curl -fsSL https://deb.nodesource.com/setup_26.x | bash - +apt-get install -y nodejs apt-get clean rm -rf /var/lib/apt/lists/* -INSTALL_PODMAN_CLIENT +INSTALL_DEV_UTILS + +RUN bash <<'INSTALL_NPM_PACKAGES' +set -euo pipefail +npm -g update +npm config set allow-scripts=@kilocode/cli --location=user +npm install -g @kilocode/cli +INSTALL_NPM_PACKAGES # Wire up the root user's .bashrc to source the Spack environment via # /entrypoint.sh on every interactive shell. diff --git a/.devcontainer/codespace.code-workspace b/.devcontainer/codespace.code-workspace index c208ed9c2..b7b289bf1 100644 --- a/.devcontainer/codespace.code-workspace +++ b/.devcontainer/codespace.code-workspace @@ -114,7 +114,6 @@ "extensions": { "recommendations": [ "charliermarsh.ruff", - "github.copilot-chat", "github.vscode-github-actions", "github.vscode-pull-request-github", "ms-vscode.cmake-tools", diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2f715e037..91b3276d7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,48 +1,17 @@ { "name": "Phlex CI Dev Container", - "build": { - "dockerfile": "Dockerfile" - }, - "runArgs": [], + "dockerComposeFile": "docker-compose.yml", + "service": "phlex-dev", "workspaceFolder": "/workspaces/phlex", "remoteUser": "root", "remoteEnv": { - // KILO_CONFIG_CONTENT_DOCKER rewrites both 127.0.0.1 headroom ports - // (azure :9797, ow :9798) to their host.docker.internal:RELAY_PORT - // (port+10000) for the socat relays used when headroom runs as a local - // split proxy pair. Passed through here so that - // post-create.sh can wire it into the container's .bashrc - // conditionally: if non-empty it sets KILO_CONFIG_CONTENT to this - // value; otherwise KILO_CONFIG_CONTENT is left unset and Kilo falls - // back to ~/.config/kilo/kilo.jsonc (bind-mounted from the host). - "KILO_CONFIG_CONTENT_DOCKER": "${localEnv:KILO_CONFIG_CONTENT_DOCKER}", "KILO_API_KEY": "${localEnv:HEADROOM_UPSTREAM_KEY}" }, - "containerEnv": { - "CMAKE_GENERATOR": "Ninja", - "GH_CONFIG_DIR": "/root/.config/gh", - "DOCKER_HOST": "unix:///tmp/podman.sock", - "CONTAINER_HOST": "unix:///tmp/podman.sock", - "GNUPGHOME": "/root/.gnupg" - }, - "mounts": [ - "source=${localWorkspaceFolder}/../phlex-coding-guidelines,target=/workspaces/phlex-coding-guidelines,type=bind", - "source=${localWorkspaceFolder}/../phlex-design,target=/workspaces/phlex-design,type=bind", - "source=${localWorkspaceFolder}/../phlex-examples,target=/workspaces/phlex-examples,type=bind", - "source=${localWorkspaceFolder}/../phlex-spack-recipes,target=/workspaces/phlex-spack-recipes,type=bind", - "source=${localEnv:HOME}/.aws,target=/root/.aws,type=bind", - "source=${localEnv:HOME}/.config/gh,target=/root/.config/gh,type=bind,readonly", - "source=${localEnv:HOME}/.config/kilo,target=/root/.config/kilo,type=bind", - "source=${localEnv:HOME}/.gnupg,target=/root/.gnupg,type=bind", - "source=${localEnv:HOME}/.kiro,target=/root/.kiro,type=bind", - "source=phlex-kilo-data,target=/root/.local/share/kilo,type=volume", - "source=${localEnv:HOME}/.podman-proxy/podman.sock,target=/tmp/podman.sock,type=bind", - "source=${localEnv:HOME}/.vscode-remote-user-data,target=/root/.vscode-server-insiders/data/User,type=bind" - ], "initializeCommand": "bash .devcontainer/ensure-repos.sh", "onCreateCommand": "bash .devcontainer/setup-repos.sh /workspaces", "postCreateCommand": "bash -lc 'bash .devcontainer/post-create.sh'", "customizations": { + "vscode": { "settings": { "terminal.integrated.defaultProfile.linux": "bash", @@ -89,7 +58,6 @@ "**/.venv/**": true, "**/py_virtual_env/**": true }, - "github.copilot-chat.usePreReleaseVersion": false, "kilocode.new.extraCaCerts": "" }, "extensions": [ @@ -100,7 +68,6 @@ "donjayamanne.githistory", "dotjoshjohnson.xml", "eamodio.gitlens", - "github.copilot-chat", "github.vscode-github-actions", "github.vscode-pull-request-github", "jebbs.plantuml", @@ -115,13 +82,11 @@ "ms-python.vscode-pylance", "ms-python.vscode-python-envs", "ms-vscode.cmake-tools", - "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-themes", "ms-vscode.hexeditor", "ms-vscode.live-server", "ms-vscode.makefile-tools", - "ms-vscode.vscode-websearchforcopilot", "redhat.vscode-yaml", "shd101wyy.markdown-preview-enhanced", "swyddfa.esbonio", diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..3c40de96d --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.8" + +services: + phlex-dev: + build: + context: . + dockerfile: Dockerfile + volumes: + - ..:/workspaces/phlex + - ../../phlex-coding-guidelines:/workspaces/phlex-coding-guidelines + - ../../phlex-design:/workspaces/phlex-design + - ../../phlex-examples:/workspaces/phlex-examples + - ../../phlex-spack-recipes:/workspaces/phlex-spack-recipes + - ${HOME}/.config/gh:/root/.config/gh:ro + - ${HOME}/.config/kilo:/root/.config/kilo + - ${HOME}/.gnupg:/root/.gnupg + - ${HOME}/.podman-proxy/podman.sock:/tmp/podman.sock + - phlex-kilo-data:/root/.local/share/kilo + - phlex-vscode-user-data:/root/.vscode-server-insiders/data/User + env_file: + - ${HOME}/.phlex-kilo.env + environment: + - CMAKE_GENERATOR=Ninja + - GH_CONFIG_DIR=/root/.config/gh + - DOCKER_HOST=unix:///tmp/podman.sock + - CONTAINER_HOST=unix:///tmp/podman.sock + - GNUPGHOME=/root/.gnupg + user: root + tty: true + stdin_open: true + +volumes: + phlex-kilo-data: + phlex-vscode-user-data: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index f58b197b9..e6e4f8b0a 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -17,15 +17,6 @@ EOF # installation on every rebuild. rm -f /root/.vscode-server-insiders/data/Machine/.installExtensionsMarker -# Set KILO_CONFIG_CONTENT for interactive shells. When KILO_CONFIG_CONTENT_DOCKER -# is non-empty (headroom local proxy via socat relay), use it so that the baseURL -# points at host.docker.internal rather than 127.0.0.1. Otherwise leave -# KILO_CONFIG_CONTENT unset so Kilo falls back to ~/.config/kilo/kilo.jsonc, -# which is bind-mounted from the host and may point at an external provider. -if [ -n "${KILO_CONFIG_CONTENT_DOCKER:-}" ]; then - printf 'export KILO_CONFIG_CONTENT=%q\n' "${KILO_CONFIG_CONTENT_DOCKER}" >> /root/.bashrc -fi - # Seed the Kilo Code auth token into the container-private data volume. # The volume is not shared with the host to avoid SQLite conflicts between # the Remote-SSH and devcontainer Kilo Code instances. The API key is @@ -106,6 +97,3 @@ if command -v prek >/dev/null 2>&1; then elif command -v pre-commit >/dev/null 2>&1; then pre-commit install || true fi - -# Install kiro-cli and set up shell integrations. -curl -fsSL https://cli.kiro.dev/install | bash From 327f1648da0238d89b03f5e4cc8153d78c7bcc80 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Wed, 8 Jul 2026 07:54:05 -0500 Subject: [PATCH 3/7] chore(devcontainer): optimize curl installation in Dockerfile --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 68a72a493..8295f197a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -95,7 +95,7 @@ set -euo pipefail apt-get update apt-get install -y --no-install-recommends jq podman ssh socat tree yq apt purge nodejs npm -y && apt autoremove -y || true -sudo apt-get install -y curl +apt-get install -y curl --no-install-recommends --autoremove curl -fsSL https://deb.nodesource.com/setup_26.x | bash - apt-get install -y nodejs apt-get clean From 1f5881d887979020e7b3ce99cbf6c302c3cebd86 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Fri, 10 Jul 2026 16:30:55 -0500 Subject: [PATCH 4/7] refactor(devcontainer): replace named volume with host bind mount for Kilo data - Switch `phlex-kilo-data` named volume to host bind mount at `${HOME}/.local/share/kilo:ro` in docker-compose.yml - Remove env_file reference to `${HOME}/.phlex-kilo.env` - Update ensure-repos.sh to create required directories with secure permissions (0700) and add `.phlex-devcontainer-tmp` - Add post-create.sh logic to rewrite Kilo config baseURLs for headroom proxy relays via socat, enabling `host.docker.internal` access from rootless Podman's pasta networking --- .devcontainer/docker-compose.yml | 5 +---- .devcontainer/ensure-repos.sh | 8 ++++---- .devcontainer/post-create.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 3c40de96d..49ac938b8 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -15,10 +15,8 @@ services: - ${HOME}/.config/kilo:/root/.config/kilo - ${HOME}/.gnupg:/root/.gnupg - ${HOME}/.podman-proxy/podman.sock:/tmp/podman.sock - - phlex-kilo-data:/root/.local/share/kilo + - ${HOME}/.local/share/kilo:/root/.local/share/kilo:ro - phlex-vscode-user-data:/root/.vscode-server-insiders/data/User - env_file: - - ${HOME}/.phlex-kilo.env environment: - CMAKE_GENERATOR=Ninja - GH_CONFIG_DIR=/root/.config/gh @@ -30,5 +28,4 @@ services: stdin_open: true volumes: - phlex-kilo-data: phlex-vscode-user-data: diff --git a/.devcontainer/ensure-repos.sh b/.devcontainer/ensure-repos.sh index 38a599f59..ef8ea76aa 100755 --- a/.devcontainer/ensure-repos.sh +++ b/.devcontainer/ensure-repos.sh @@ -200,10 +200,10 @@ relay_headroom_port azure "${HEADROOM_AZURE_PORT}" relay_headroom_port ow "${HEADROOM_OW_PORT}" # Ensure remaining source bind mount points exist. -ensure_bind_dir "$HOME/.aws" ensure_bind_dir "$HOME/.config/"{gh,kilo} -ensure_bind_dir "$HOME/.gnupg" -ensure_bind_dir "$HOME/.kiro" -ensure_bind_dir "$HOME/.vscode-remote-user-data" +ensure_bind_dir -m 0700 "$HOME/.gnupg" +ensure_bind_dir -m 0700 "$HOME/.vscode-remote-user-data" +ensure_bind_dir -m 0700 "$HOME/.local/share/kilo" +ensure_bind_dir -m 0700 "$HOME/.phlex-devcontainer-tmp" echo "SUCCESS: .devcontainer/ensure-repos.sh completed successfully" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index e6e4f8b0a..75fecde39 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -97,3 +97,33 @@ if command -v prek >/dev/null 2>&1; then elif command -v pre-commit >/dev/null 2>&1; then pre-commit install || true fi + +# Configure Kilo to use host.docker.internal for headroom proxy ports. +# The headroom SSH tunnels bind to 127.0.0.1 only, but rootless Podman's +# pasta networking only routes host.docker.internal to ports listening on +# 0.0.0.0. We use socat relays (set up in ensure-repos.sh) to forward +# 127.0.0.1:9797 -> 0.0.0.0:19797 and 127.0.0.1:9798 -> 0.0.0.0:19798. +# This code rewrites Kilo's baseURL from 127.0.0.1: to +# host.docker.internal: for the relays to work. +cat >> /root/.bashrc <<'EOF' + +# Unset any previous KILO_CONFIG_CONTENT to ensure a clean slate. +unset KILO_CONFIG_CONTENT + +# If the headroom proxy ports (9797, 9798) are active via socat relays, +# Kilo needs a modified config with baseURL pointing to host.docker.internal. +# Parse 'kilo debug config', rewrite baseURLs, and export as KILO_CONFIG_CONTENT. +if command -v kilo >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + # Only rewrite if we have a working Kilo config to work with. + if KILO_CONFIG_CONTENT="$(perl -pe 's{("(?:[^"\\]|\\.)*"|'"'"'(?:[^'"'"'\\]|\\.)*'"'"')|//.*}{$1 // ""}ge' "${HOME}/.config/kilo/kilo.jsonc" | jq -c '.provider |= with_entries( + .value.options.baseURL = + (if .value.options.baseURL == null then null + elif (.value.options.baseURL | test("^https?://127\\.0\\.0\\.1:")) + then (.value.options.baseURL | capture("https?://127\\.0\\.0\\.1:(?[0-9]+)(?.*)") | "http://host.docker.internal:" + ((.port | tonumber + 10000) | tostring) + .rest) + else .value.options.baseURL + end) + )')"; then + export KILO_CONFIG_CONTENT + fi +fi +EOF From 4ad0e959fe9cb0d08a1daa54f7c4813b9a755288 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Thu, 16 Jul 2026 15:44:43 -0500 Subject: [PATCH 5/7] Remove global `npm update` - Per https://github.com/Framework-R-D/phlex/pull/681#discussion_r3501508895 @coderabbitai `@kilocode/cli` is left unpinned due to active development and regular improvements. --- .devcontainer/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8295f197a..ad5eefc08 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -104,7 +104,6 @@ INSTALL_DEV_UTILS RUN bash <<'INSTALL_NPM_PACKAGES' set -euo pipefail -npm -g update npm config set allow-scripts=@kilocode/cli --location=user npm install -g @kilocode/cli INSTALL_NPM_PACKAGES From 50b4691ed4da3e35c379320d1f49cee3f16b08f0 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Thu, 16 Jul 2026 15:54:24 -0500 Subject: [PATCH 6/7] Remove unwanted changes to `git-ai-commit` and friends. - See #682 --- scripts/git-ai-commit | 44 ++++++++++-------------------- scripts/man/man1/git-ai-commit.1 | 10 ++----- scripts/test/test_git_ai_commit.py | 42 ++-------------------------- 3 files changed, 19 insertions(+), 77 deletions(-) diff --git a/scripts/git-ai-commit b/scripts/git-ai-commit index a07dfb653..99cea172a 100755 --- a/scripts/git-ai-commit +++ b/scripts/git-ai-commit @@ -77,12 +77,11 @@ Environment variables GIT_AI_COMMIT_KILO_API kilo backend base URL (default: https://litellm.fnal.gov/v1) GIT_AI_COMMIT_KILO_MODEL kilo backend default model (default: azure/claude-haiku-4-5); setting this env var suppresses auto-escalation - GIT_AI_COMMIT_KILO_PROVIDER kilo auth.json provider key (default: tries - fnal-azure, then fnal-ow, then legacy fnal-litellm) + GIT_AI_COMMIT_KILO_PROVIDER kilo auth.json provider key (default: fnal-litellm) Token resolution (kilo backend): 1. GIT_AI_COMMIT_TOKEN - 2. ~/.local/share/kilo/auth.json (fnal-azure / fnal-ow key) + 2. ~/.local/share/kilo/auth.json (fnal-litellm key) 3. GITHUB_TOKEN / GH_TOKEN 4. `gh auth token` 5. ~/.config/gh/hosts.yml @@ -129,18 +128,7 @@ _DEFAULT_MODEL_KILO = _KILO_MODEL_ENV or "azure/claude-haiku-4-5" # True when the user has explicitly pinned a kilo model via the environment, # which suppresses auto-escalation even if the diff is large. _KILO_MODEL_PINNED_BY_ENV = bool(_KILO_MODEL_ENV) -# auth.json provider key(s) that hold the FNAL gateway API token. The former -# single "fnal-litellm" provider was split into "fnal-azure" and "fnal-ow", -# which share the same upstream key; "fnal-litellm" is retired. When the user -# does not pin GIT_AI_COMMIT_KILO_PROVIDER, try the new keys in order (and the -# retired name last, for any un-migrated auth.json). git-ai-commit talks to -# the LiteLLM gateway directly, so the provider key only selects the auth -# token — model names remain bare (e.g. "azure/...", "qwen/..."). -_KILO_PROVIDER_ENV = os.environ.get("GIT_AI_COMMIT_KILO_PROVIDER", "").strip() -_DEFAULT_KILO_PROVIDER = _KILO_PROVIDER_ENV or "fnal-azure" -_KILO_PROVIDER_FALLBACKS = ( - [_KILO_PROVIDER_ENV] if _KILO_PROVIDER_ENV else ["fnal-azure", "fnal-ow", "fnal-litellm"] -) +_DEFAULT_KILO_PROVIDER = os.environ.get("GIT_AI_COMMIT_KILO_PROVIDER", "fnal-litellm") _DEFAULT_MODEL_OTHER = "gpt-4o" _MAX_DIFF_CHARS = 60_000 @@ -384,11 +372,8 @@ def _get_instructions(root: Path) -> str: def _kilo_auth_token() -> str | None: """Read the kilo provider API key from kilo's auth.json, if present. - The provider name(s) come from GIT_AI_COMMIT_KILO_PROVIDER when set; - otherwise the split FNAL providers are tried in order - ("fnal-azure", "fnal-ow", then the retired "fnal-litellm"). These share - the same upstream gateway token, so the first present key wins. The names - match the provider keys in ~/.config/kilo/kilo.jsonc. + The provider name is taken from GIT_AI_COMMIT_KILO_PROVIDER (default: + "fnal-litellm"), matching the key used in ~/.config/kilo/kilo.jsonc. """ if not _KILO_AUTH_JSON.exists(): return None @@ -398,14 +383,13 @@ def _kilo_auth_token() -> str | None: return None if not isinstance(data, dict): return None - for provider_name in _KILO_PROVIDER_FALLBACKS: - provider = data.get(provider_name) - if not isinstance(provider, dict): - continue - key = provider.get("key", "") - if isinstance(key, str) and key.strip(): - return key.strip() - return None + provider = data.get(_DEFAULT_KILO_PROVIDER) + if not isinstance(provider, dict): + return None + key = provider.get("key", "") + if not isinstance(key, str): + return None + return key.strip() or None def _gh_config_token() -> str | None: @@ -478,8 +462,8 @@ def _token(backend: str) -> str: if backend == "kilo": raise _Error( "No FNAL LiteLLM token found. " - "Ensure kilo is configured with the fnal-azure or fnal-ow " - "provider, or set GIT_AI_COMMIT_TOKEN." + "Ensure kilo is configured with the fnal-litellm provider, or " + "set GIT_AI_COMMIT_TOKEN." ) raise _Error( "No API token found. Set GIT_AI_COMMIT_TOKEN, GITHUB_TOKEN, or run 'gh auth login'." diff --git a/scripts/man/man1/git-ai-commit.1 b/scripts/man/man1/git-ai-commit.1 index 2d6793a0c..0600e6a13 100644 --- a/scripts/man/man1/git-ai-commit.1 +++ b/scripts/man/man1/git-ai-commit.1 @@ -136,7 +136,7 @@ Token resolution order: .IP "1." 4 \fBGIT_AI_COMMIT_TOKEN\fR .IP "2." 4 -\fB~/.local/share/kilo/auth.json\fR (\fIfnal\-azure\fR / \fIfnal\-ow\fR key) +\fB~/.local/share/kilo/auth.json\fR (\fIfnal\-litellm\fR key) .IP "3." 4 \fBGITHUB_TOKEN\fR / \fBGH_TOKEN\fR .IP "4." 4 @@ -197,10 +197,7 @@ Default model for the \fBkilo\fR backend. Default: \fBazure/claude\-haiku\-4\-5 Setting this variable suppresses auto\-escalation even for large prompts. .TP \fBGIT_AI_COMMIT_KILO_PROVIDER\fR -Provider key in \fB~/.local/share/kilo/auth.json\fR. When unset, the split -FNAL providers are tried in order: \fBfnal\-azure\fR, then \fBfnal\-ow\fR, then -the retired \fBfnal\-litellm\fR. These share the same upstream gateway token, -so the first key present is used. +Provider key in \fB~/.local/share/kilo/auth.json\fR. Default: \fBfnal\-litellm\fR. .TP \fBEDITOR\fR, \fBVISUAL\fR Editor invoked by \fB\-\-edit\fR. Checked in order; \fBvi\fR is the final fallback. @@ -249,8 +246,7 @@ echo "Closes #1234; keep message terse" | git ai\-commit \-\-yes .SH FILES .TP \fB~/.local/share/kilo/auth.json\fR -Kilo credentials file; read for the \fBfnal\-azure\fR / \fBfnal\-ow\fR provider -key (legacy \fBfnal\-litellm\fR still honored if present). +Kilo credentials file; read for the \fBfnal\-litellm\fR provider key. .TP \fB~/.config/git\-ai\-commit/instructions.md\fR User\-level instructions included in every prompt. diff --git a/scripts/test/test_git_ai_commit.py b/scripts/test/test_git_ai_commit.py index a527a1700..cbce95ee0 100644 --- a/scripts/test/test_git_ai_commit.py +++ b/scripts/test/test_git_ai_commit.py @@ -67,7 +67,7 @@ class TestKiloAuthToken: """_kilo_auth_token validates the kilo credentials file before reading.""" - _PROVIDER = "fnal-azure" + _PROVIDER = "fnal-litellm" def _write(self, path: Path, obj: object) -> None: path.parent.mkdir(parents=True, exist_ok=True) @@ -75,9 +75,7 @@ def _write(self, path: Path, obj: object) -> None: def _setup(self, monkeypatch: pytest.MonkeyPatch, path: Path) -> None: monkeypatch.setattr(_M, "_KILO_AUTH_JSON", path) - # _kilo_auth_token iterates _KILO_PROVIDER_FALLBACKS; pin it to a single - # provider for the single-provider test cases below. - monkeypatch.setattr(_M, "_KILO_PROVIDER_FALLBACKS", [self._PROVIDER]) + monkeypatch.setattr(_M, "_DEFAULT_KILO_PROVIDER", self._PROVIDER) def test_file_missing_returns_none( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -175,42 +173,6 @@ def test_valid_key_no_whitespace( self._setup(monkeypatch, p) assert _kilo_auth_token() == "tok123" - def test_provider_fallback_order( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - """First present provider in the fallback list wins. - - The retired fnal-litellm provider was split into fnal-azure and - fnal-ow; _kilo_auth_token tries the list in order and returns the first - provider that has a usable key, skipping absent earlier entries. - """ - p = tmp_path / "auth.json" - # fnal-azure absent; fnal-ow present -> fnal-ow key is used. - self._write(p, {"fnal-ow": {"key": "ow-token"}}) - monkeypatch.setattr(_M, "_KILO_AUTH_JSON", p) - monkeypatch.setattr( - _M, "_KILO_PROVIDER_FALLBACKS", ["fnal-azure", "fnal-ow", "fnal-litellm"] - ) - assert _kilo_auth_token() == "ow-token" - - def test_provider_fallback_prefers_earlier( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - """When multiple providers are present, the earliest in the list wins.""" - p = tmp_path / "auth.json" - self._write( - p, - { - "fnal-azure": {"key": "azure-token"}, - "fnal-ow": {"key": "ow-token"}, - }, - ) - monkeypatch.setattr(_M, "_KILO_AUTH_JSON", p) - monkeypatch.setattr( - _M, "_KILO_PROVIDER_FALLBACKS", ["fnal-azure", "fnal-ow", "fnal-litellm"] - ) - assert _kilo_auth_token() == "azure-token" - # =========================================================================== # _gh_cli_token From 29578c1c88147559f7078f6c0116149b6de090e5 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Fri, 17 Jul 2026 15:42:04 -0500 Subject: [PATCH 7/7] Use tag `latest` for `FROM` image --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ad5eefc08..08bc3653d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/framework-r-d/phlex-dev:2026-06-24 +FROM ghcr.io/framework-r-d/phlex-dev:latest # Validate Python site-packages symlink. # Invoke bash explicitly: the script uses bash features (pipefail, arrays,