Skip to content
Open
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
21 changes: 16 additions & 5 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
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 -g update

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Drop npm -g update from the image build.

This makes the devcontainer drift on every rebuild against the live registry, which undercuts the new pinning work here. Install only the tool you need, ideally at an explicit @kilocode/cli version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/Dockerfile at line 107, Remove the global npm update step from
the devcontainer image build so the image does not drift against the live
registry on each rebuild. Update the Dockerfile build steps to install only the
required tool, and pin that install to an explicit `@kilocode/cli` version instead
of relying on npm -g update. Use the existing Dockerfile package-install section
to make the change.

npm config set allow-scripts=@kilocode/cli --location=user
npm install -g @kilocode/cli
Comment on lines +108 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does npm support an allow-scripts config key for selectively allowing lifecycle scripts for one package, and what is the supported way to restrict install scripts in current npm?

💡 Result:

Yes, npm now supports an allowScripts configuration to selectively permit lifecycle scripts for specific packages, a feature introduced as part of an opt-in security policy in recent versions of npm (starting with v11.10.0) [1]. How to Manage Lifecycle Scripts: 1. Project-Level Control (Recommended): The supported way to manage this in a project is using the allowScripts field in your package.json [2][3]. You can maintain this list using two built-in commands: - npm approve-scripts : Adds a package to the allowlist in your package.json [3][4]. By default, this pins the approval to the specific version currently installed [3][5]. - npm deny-scripts : Explicitly denies a package, ensuring it cannot run install scripts even if future versions are updated [4]. 2. One-off and Global Contexts: For global installs (npm install -g) or one-off executions (npm exec, npx), where no project package.json exists, you can use the --allow-scripts CLI flag (e.g., --allow-scripts=pkg1,pkg2) [2][3][6]. Note that using --allow-scripts during a project-scoped npm install is considered an error, as team-wide policy should be managed via package.json or.npmrc to avoid inconsistency [2][7][6]. 3. Legacy/Alternative Restriction: If you want to globally restrict install scripts (the most secure approach), you can continue to use the ignore-scripts configuration [8][9][10]. - You can set this globally by running: npm config set ignore-scripts true [10]. - Alternatively, you can run any install command with the flag: npm install --ignore-scripts [8][9][10]. In the current version, the allowScripts policy is advisory; unapproved scripts will still execute, but they will be reported in an end-of-install summary [11][1][4]. Future releases of npm are planned to make these policies a hard block [2][1][4]. Top results: [2][3][11][6][4]

Citations:


allow-scripts doesn’t harden this install.
npm config set allow-scripts=@kilocode/cli is policy metadata, not a blocker, so `npm install -g `@kilocode/cli can still run lifecycle scripts. If the goal is to prevent install-time script execution, use --ignore-scripts or the npm allowlist mechanism supported by the image’s npm version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/Dockerfile around lines 108 - 109, The install flow in the
Dockerfile still allows lifecycle scripts to run because `npm config set
allow-scripts=...` is only policy metadata. Update the `npm install -g
`@kilocode/cli`` step to actually block install-time scripts, using
`--ignore-scripts` or the npm allowlist mechanism supported by this image’s npm
version, and keep the change localized to the install commands in the
Dockerfile.

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
34 changes: 34 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +20 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

This env_file is mandatory, but its producer is conditional.

.devcontainer/ensure-repos.sh:189-199 only creates ${HOME}/.phlex-kilo.env when ~/.config/kilo/kilo.jsonc exists. On a fresh machine without that file, Compose will try to load a missing env file and the devcontainer can fail before startup. Either make ensure-repos.sh always write an empty .phlex-kilo.env, or switch this wiring to an optional env-file path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/docker-compose.yml around lines 20 - 21, The devcontainer
compose config currently treats ${HOME}/.phlex-kilo.env as required even though
ensure-repos.sh only creates it conditionally, so update the wiring to avoid
startup failure when the file is missing. Either change ensure-repos.sh so it
always creates .phlex-kilo.env (even if empty) or adjust the env_file entry in
docker-compose.yml to use an optional/conditional path. Refer to ensure-repos.sh
and the env_file setting in docker-compose.yml when making the fix.

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:
76 changes: 46 additions & 30 deletions .devcontainer/ensure-repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:<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"
Expand Down
21 changes: 8 additions & 13 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 @@ -99,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
44 changes: 30 additions & 14 deletions scripts/git-ai-commit
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@
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
Expand Down Expand Up @@ -128,7 +129,18 @@
# 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"

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable '_DEFAULT_KILO_PROVIDER' is not used.
_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
Expand Down Expand Up @@ -372,8 +384,11 @@
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
Expand All @@ -383,13 +398,14 @@
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:
Expand Down Expand Up @@ -462,8 +478,8 @@
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'."
Expand Down
Loading
Loading