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
68 changes: 43 additions & 25 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04

# Install firewall dependencies and pixi
# System packages: firewall tooling (iptables/ipset/dns/jq/aggregate), curl, and
# the sandbox helpers bubblewrap + socat.
RUN apt-get update && apt-get install -y --no-install-recommends \
iptables \
ipset \
dnsutils \
jq \
aggregate \
curl \
ca-certificates \
gnupg \
bubblewrap \
socat \
&& rm -rf /var/lib/apt/lists/*

# Install Playwright browser dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libglib2.0-0t64 \
libnspr4 \
libnss3 \
libdbus-1-3 \
libatk1.0-0t64 \
libatk-bridge2.0-0t64 \
libcups2t64 \
libxcb1 \
libxkbcommon0 \
libatspi2.0-0t64 \
libx11-6 \
libxcomposite1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libcairo2 \
libpango-1.0-0 \
libasound2t64 \
# Node.js, build-time only: needed to install the Codex CLI and the Playwright
# browser at image-build time (when no credentials are mounted). The project
# itself uses pixi's own Node for builds at runtime.
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# Install pixi system-wide
RUN curl -fsSL https://pixi.sh/install.sh | PIXI_HOME=/usr/local bash

# Copy firewall init script
# Playwright browser + OS deps, pinned to the project's version, in a shared path
# readable by the vscode user. The config defines no projects, so only chromium
# is needed. Done at build time so the runtime (firewalled) phase never needs the
# dynamic Playwright CDN.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
RUN npx --yes playwright@1.56.1 install --with-deps chromium \
&& rm -rf /var/lib/apt/lists/*

# Codex CLI (global). Build-time install -> no credentials present to leak.
RUN npm install -g @openai/codex

# Claude Code (native installer) as the vscode user so it lands in
# /home/vscode/.local/bin and matches installMethod "native".
USER vscode
RUN curl -fsSL https://claude.ai/install.sh | HOME=/home/vscode bash
USER root

# Firewall script + root entrypoint that brings it up before any workload runs.
COPY init-firewall.sh /usr/local/bin/init-firewall.sh
RUN chmod +x /usr/local/bin/init-firewall.sh
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh

# Bake "no sudo for vscode" into the image: the agent runs unprivileged and can
# never escalate or modify the firewall. Privileged setup (firewall init, volume
# chown) happens in the root entrypoint instead. For maintenance, exec as root
# from the host: `podman exec -u root <container> bash`.
RUN rm -f /etc/sudoers.d/vscode

# Start as root so the entrypoint can set up the firewall, then exec the
# long-running command. The devcontainers CLI execs all lifecycle commands and
# shells as the unprivileged remoteUser (vscode).
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["sleep", "infinity"]
16 changes: 15 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,22 @@
"remoteEnv": {
"NODE_OPTIONS": "--max-old-space-size=2048",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_DISABLE_ANALYTICS": "1"
"CLAUDE_CODE_DISABLE_ANALYTICS": "1",
"PLAYWRIGHT_BROWSERS_PATH": "/opt/ms-playwright"
},
"containerEnv": {
// Keep pixi's cache on the xfs .pixi volume so it applies to every process
// in the container (entrypoint, postCreate, and all exec sessions). The
// container rootfs (~/.cache) is fuse-overlayfs under rootless Podman, which
// pixi flags as a network FS and redirects per-run; the .pixi volume is
// local xfs and persists. ${containerWorkspaceFolder} = /workspaces/fileglancer.
"PIXI_CACHE_DIR": "${containerWorkspaceFolder}/.pixi/.pixi-cache"
},
"postCreateCommand": "bash .devcontainer/post-create.sh",
// Start the container as root so the entrypoint can initialize the firewall
// and fix volume ownership; all lifecycle commands and shells still run as the
// unprivileged vscode user. overrideCommand=false lets our ENTRYPOINT/CMD run.
"overrideCommand": false,
"containerUser": "root",
"remoteUser": "vscode"
}
31 changes: 31 additions & 0 deletions .devcontainer/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Root entrypoint: runs at container start, BEFORE any workload or agent. Brings
# up the egress firewall and fixes volume ownership, then execs the long-running
# command. The devcontainers CLI execs all lifecycle commands and shells as the
# unprivileged vscode user, which has no sudo (removed in the image).
set -euo pipefail

WORKSPACE="/workspaces/fileglancer"
READY_FLAG="/run/fg-firewall-ready"

rm -f "$READY_FLAG"

# Fix ownership of the mounted .pixi volume if it isn't already the dev user's.
# On rootless Podman (keep-id) it's already 1000:1000 -> no-op. On Docker/Colima
# named volumes are created root-owned, so chown them to vscode.
if [ -d "$WORKSPACE/.pixi" ] && [ "$(stat -c %u "$WORKSPACE/.pixi")" != "1000" ]; then
echo "entrypoint: fixing ownership of $WORKSPACE/.pixi"
chown -R 1000:1000 "$WORKSPACE/.pixi" || true
fi

# Bring up the egress allowlist firewall before the agent can run. Fail closed:
# if firewall setup fails, the entrypoint exits and the container does not start.
echo "entrypoint: initializing egress firewall"
/usr/local/bin/init-firewall.sh

# Signal readiness so post-create (run by the CLI as vscode) waits for the
# firewall before doing any network access.
touch "$READY_FLAG"

echo "entrypoint: setup complete, starting: $*"
exec "$@"
63 changes: 24 additions & 39 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -1,50 +1,35 @@
#!/bin/bash
set -e

# Fix ownership of .pixi volume (created as root by Docker)
sudo chown -R "$(id -u):$(id -g)" .pixi 2>/dev/null || true
# This phase runs as the unprivileged vscode user, AFTER the root entrypoint has
# brought up the egress firewall. It has NO sudo and NO access to non-allowlisted
# CDNs: Claude, Codex, the Playwright browser, and the firewall are all handled
# at image-build / entrypoint time. Only steps that need the mounted workspace
# remain, and they hit only allowlisted endpoints (pypi/conda/prefix.dev/npm).

# Wait for the entrypoint's firewall to be in place before any network access.
echo "Waiting for egress firewall to come up..."
for _ in $(seq 1 60); do
[ -f /run/fg-firewall-ready ] && break
sleep 1
done
if [ ! -f /run/fg-firewall-ready ]; then
echo "ERROR: firewall readiness flag not found; aborting setup" >&2
exit 1
fi

# Initialize pixi environment and install package dependencies
echo "Installing pixi environment..."
pixi install
# Install the pixi environment from the lockfile (no resolution drift).
echo "Installing pixi environment (locked)..."
pixi install --locked

# Install fileglancer in development mode (builds frontend + installs Python package)
echo "Running dev-install (this builds frontend and installs the package)..."
# Build the frontend and install the Python package in development mode.
echo "Running dev-install (frontend build + Python package)..."
pixi run dev-install

# Install Playwright browsers for UI tests (before firewall, as CDN IPs are dynamic)
echo "Installing Playwright browsers..."
# Install UI-test JS deps (the @playwright/test package). The matching browser
# is already baked into the image at PLAYWRIGHT_BROWSERS_PATH.
echo "Installing UI-test dependencies..."
pixi run node-install-ui-tests
cd frontend/ui-tests && pixi run npx playwright install

# Install Claude Code via the native installer (before firewall, since claude.ai
# CDN isn't in the allowlist). Installs to ~/.local/bin/claude, matching the
# "installMethod: native" recorded in the bind-mounted ~/.claude.json from the host.
if ! [ -x "$HOME/.local/bin/claude" ]; then
echo "Installing Claude Code (native)..."
curl -fsSL https://claude.ai/install.sh | bash
fi

# Initialize network firewall (restricts outbound to allowed domains)
# This must happen AFTER Playwright and Claude installs since their CDN IPs are dynamic
echo "Initializing network firewall..."
sudo /usr/local/bin/init-firewall.sh

# Install Codex CLI globally via npm (provided by pixi)
if ! command -v codex &> /dev/null; then
echo "Installing Codex CLI..."
pixi run npm install -g @openai/codex
fi

# Lock down the firewall: now that setup is complete, revoke the vscode user's
# passwordless sudo. The agent runs as unprivileged vscode, which cannot touch
# iptables/ipset without root, so the egress allowlist can no longer be flushed
# or bypassed from inside the container. NET_ADMIN/NET_RAW remain in the image
# but are unusable without root, so this neutralizes them for the agent.
# For maintenance you can still get a root shell from the HOST side:
# podman exec -u root <container> bash (or: docker exec -u root ...)
echo "Revoking in-container sudo to lock the firewall..."
sudo rm -f /etc/sudoers.d/vscode

echo ""
echo "=========================================="
Expand Down
Loading
Loading