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
18 changes: 14 additions & 4 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,25 @@ 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
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
rm -rf /var/lib/apt/lists/*
INSTALL_PODMAN_CLIENT
INSTALL_DEV_UTILS

RUN bash <<'INSTALL_NPM_PACKAGES'
set -euo pipefail
npm config set allow-scripts=@kilocode/cli --location=user
npm install -g @kilocode/cli
Comment thread
coderabbitai[bot] marked this conversation as resolved.
INSTALL_NPM_PACKAGES

# Wire up the root user's .bashrc to source the Spack environment via
# /entrypoint.sh on every interactive shell.
Expand Down
1 change: 0 additions & 1 deletion .devcontainer/codespace.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@
"extensions": {
"recommendations": [
"charliermarsh.ruff",
"github.copilot-chat",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"ms-vscode.cmake-tools",
Expand Down
40 changes: 3 additions & 37 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,47 +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 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
// 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",
Expand Down Expand Up @@ -88,7 +58,6 @@
"**/.venv/**": true,
"**/py_virtual_env/**": true
},
"github.copilot-chat.usePreReleaseVersion": false,
"kilocode.new.extraCaCerts": ""
},
"extensions": [
Expand All @@ -99,7 +68,6 @@
"donjayamanne.githistory",
"dotjoshjohnson.xml",
"eamodio.gitlens",
"github.copilot-chat",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"jebbs.plantuml",
Expand All @@ -114,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",
Expand Down
31 changes: 31 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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
- ${HOME}/.local/share/kilo:/root/.local/share/kilo:ro
- phlex-vscode-user-data:/root/.vscode-server-insiders/data/User
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-vscode-user-data:
84 changes: 50 additions & 34 deletions .devcontainer/ensure-repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,44 +150,60 @@ 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:<port+10000>. A different
# port is required because 0.0.0.0:<port> would conflict with the existing
# 127.0.0.1:<port> 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"
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"
49 changes: 37 additions & 12 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,10 +30,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",
Expand Down Expand Up @@ -100,5 +98,32 @@ 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
# 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:<port> to
# host.docker.internal:<port+10000> 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:(?<port>[0-9]+)(?<rest>.*)") | "http://host.docker.internal:" + ((.port | tonumber + 10000) | tostring) + .rest)
else .value.options.baseURL
end)
)')"; then
export KILO_CONFIG_CONTENT
fi
fi
EOF
Loading