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

# Install firewall dependencies and pixi
RUN apt-get update && apt-get install -y --no-install-recommends \
iptables \
ipset \
dnsutils \
jq \
aggregate \
curl \
&& 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 \
&& 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
COPY init-firewall.sh /usr/local/bin/init-firewall.sh
RUN chmod +x /usr/local/bin/init-firewall.sh
44 changes: 44 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "Fileglancer",
"build": {
"dockerfile": "Dockerfile"
},
"runArgs": [
"--cap-add=NET_ADMIN",
"--cap-add=NET_RAW"
],
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"tamasfe.even-better-toml",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
],
"settings": {
"python.defaultInterpreterPath": "${workspaceFolder}/.pixi/envs/default/bin/python",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[python]": {
"editor.defaultFormatter": "ms-python.python"
}
}
}
},
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind",
"source=fileglancer-pixi,target=${containerWorkspaceFolder}/.pixi,type=volume"
],
"remoteEnv": {
"NODE_OPTIONS": "--max-old-space-size=4096",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_DISABLE_ANALYTICS": "1"
},
"postCreateCommand": "bash .devcontainer/post-create.sh",
"remoteUser": "vscode"
}
151 changes: 151 additions & 0 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# Network firewall script for Claude Code devcontainer
# Restricts outbound traffic to allowed domains only
# Based on: https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh

# 1. Extract Docker DNS info BEFORE any flushing
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)

# Flush existing rules and delete existing ipsets
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
ipset destroy allowed-domains 2>/dev/null || true

# 2. Selectively restore ONLY internal Docker DNS resolution
if [ -n "$DOCKER_DNS_RULES" ]; then
echo "Restoring Docker DNS rules..."
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
else
echo "No Docker DNS rules to restore"
fi

# First allow DNS and localhost before any restrictions
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Create ipset with CIDR support
ipset create allowed-domains hash:net

# Fetch GitHub meta information and add their IP ranges
echo "Fetching GitHub IP ranges..."
gh_ranges=$(curl -s https://api.github.com/meta)
if [ -z "$gh_ranges" ]; then
echo "ERROR: Failed to fetch GitHub IP ranges"
exit 1
fi

if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
echo "ERROR: GitHub API response missing required fields"
exit 1
fi

echo "Processing GitHub IPs..."
while read -r cidr; do
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
exit 1
fi
echo "Adding GitHub range $cidr"
ipset add allowed-domains "$cidr" -exist
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)

# Resolve and add other allowed domains
# Note: We use -exist flag on ipset add to ignore duplicates, since multiple
# domains (e.g. pypi.org and files.pythonhosted.org) can resolve to the same CDN IPs
ALLOWED_DOMAINS=(
# Claude Code / Anthropic
"api.anthropic.com"
"sentry.io"
"statsig.anthropic.com"
"statsig.com"
# npm
"registry.npmjs.org"
# VS Code
"marketplace.visualstudio.com"
"vscode.blob.core.windows.net"
"update.code.visualstudio.com"
# Python / Pixi / Conda
"pypi.org"
"files.pythonhosted.org"
"conda.anaconda.org"
"conda-mapping.prefix.dev"
"prefix.dev"
"repo.prefix.dev"
)

for domain in "${ALLOWED_DOMAINS[@]}"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
echo "WARNING: Failed to resolve $domain, skipping"
continue
fi

while read -r ip; do
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "ERROR: Invalid IP from DNS for $domain: $ip"
exit 1
fi
echo "Adding $ip for $domain"
ipset add allowed-domains "$ip" -exist
done < <(echo "$ips")
done

# Get host IP from default route
HOST_IP=$(ip route | grep default | cut -d" " -f3)
if [ -z "$HOST_IP" ]; then
echo "ERROR: Failed to detect host IP"
exit 1
fi

HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
echo "Host network detected as: $HOST_NETWORK"

# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT

# Set default policies to DROP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow outbound traffic to allowed domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT

# Reject all other outbound traffic
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited

echo "Firewall configuration complete"
echo "Verifying firewall rules..."

if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - was able to reach https://example.com"
exit 1
else
echo "Firewall verification passed - unable to reach https://example.com as expected"
fi

if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
exit 1
else
echo "Firewall verification passed - able to reach https://api.github.com as expected"
fi
43 changes: 43 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/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

# Initialize pixi environment and install package dependencies
echo "Installing pixi environment..."
pixi install

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

# Install Playwright browsers for UI tests (before firewall, as CDN IPs are dynamic)
echo "Installing Playwright browsers..."
pixi run node-install-ui-tests
cd frontend/ui-tests && pixi run npx playwright install

# Initialize network firewall (restricts outbound to allowed domains)
# This must happen AFTER Playwright install since CDN IPs change dynamically
echo "Initializing network firewall..."
sudo /usr/local/bin/init-firewall.sh

# Install Claude Code globally via npm (provided by pixi)
if ! command -v claude &> /dev/null; then
echo "Installing Claude Code..."
pixi run npm install -g @anthropic-ai/claude-code
fi

echo ""
echo "=========================================="
echo "Dev container setup complete!"
echo "=========================================="
echo ""
echo "Available commands:"
echo " claude --dangerously-skip-permissions - Start Claude Code"
echo " pixi run dev-launch - Start dev server on port 7878"
echo " pixi run dev-watch - Watch frontend for changes"
echo " pixi run test-backend - Run Python tests"
echo " pixi run test-frontend - Run frontend tests"
echo " pixi run node-check - TypeScript type checking"
echo ""
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,5 @@ config.yaml

# Claude Code
.claude
/CLAUDE.md

notifications.yaml
Loading