From 7291b5006cb903e0b7d3dc064702aa4c75de0ffa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:12:33 +0000 Subject: [PATCH 01/10] Initial plan From f28c087bcf4689136096439393ba43868bf98e6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:22:33 +0000 Subject: [PATCH 02/10] feat: add configurable agent base image for GitHub Actions runner parity Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- containers/agent/Dockerfile | 47 ++++++++++++++++------- docs/usage.md | 75 +++++++++++++++++++++++++++++++++++++ src/cli.ts | 18 +++++++++ src/docker-manager.test.ts | 21 +++++++++++ src/docker-manager.ts | 19 +++++++--- src/types.ts | 18 +++++++++ 6 files changed, 178 insertions(+), 20 deletions(-) diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 1e8af6c63..62d8e4aa9 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -1,6 +1,14 @@ -FROM ubuntu:22.04 +# BASE_IMAGE allows customization of the base Ubuntu image for closer parity +# with GitHub Actions runner environments. Options: +# - ubuntu:22.04 (default): Minimal image, smallest size (~200MB) +# - ghcr.io/catthehacker/ubuntu:runner-22.04: Closer to GitHub Actions runner (~2-5GB) +# - ghcr.io/catthehacker/ubuntu:full-22.04: Near-identical to GitHub Actions runner (~20GB compressed) +# Use --build-arg BASE_IMAGE= to customize +ARG BASE_IMAGE=ubuntu:22.04 +FROM ${BASE_IMAGE} # Install required packages and Node.js 22 +# Note: Some packages may already exist in runner-like base images, apt handles this gracefully RUN apt-get update && \ apt-get install -y --no-install-recommends \ iptables \ @@ -13,16 +21,20 @@ RUN apt-get update && \ netcat-openbsd \ gosu \ libcap2-bin && \ - # Install Node.js 22 from NodeSource - curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ - apt-get install -y nodejs && \ - # Install Docker CLI for MCP servers that run as containers - install -m 0755 -d /etc/apt/keyrings && \ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \ - chmod a+r /etc/apt/keyrings/docker.asc && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \ - apt-get update && \ - apt-get install -y docker-ce-cli && \ + # Install Node.js 22 from NodeSource (only if not already installed or wrong version) + if ! command -v node >/dev/null 2>&1 || ! node --version | grep -q "v22\."; then \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs; \ + fi && \ + # Install Docker CLI for MCP servers that run as containers (only if not already installed) + if ! command -v docker >/dev/null 2>&1; then \ + install -m 0755 -d /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \ + chmod a+r /etc/apt/keyrings/docker.asc && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \ + apt-get update && \ + apt-get install -y docker-ce-cli; \ + fi && \ rm -rf /var/lib/apt/lists/* # Create non-root user with UID/GID matching host user @@ -44,9 +56,16 @@ COPY pid-logger.sh /usr/local/bin/pid-logger.sh RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/docker-wrapper.sh /usr/local/bin/pid-logger.sh # Install docker wrapper to intercept docker commands -# Rename real docker binary and replace with wrapper -RUN mv /usr/bin/docker /usr/bin/docker-real && \ - ln -s /usr/local/bin/docker-wrapper.sh /usr/bin/docker +# Handle cases where docker may already exist (symlink or binary) +RUN if [ -f /usr/bin/docker ] && [ ! -L /usr/bin/docker ]; then \ + mv /usr/bin/docker /usr/bin/docker-real; \ + elif [ -L /usr/bin/docker ]; then \ + # Docker is a symlink, resolve and copy the target + DOCKER_TARGET=$(readlink -f /usr/bin/docker) && \ + rm /usr/bin/docker && \ + cp "$DOCKER_TARGET" /usr/bin/docker-real; \ + fi && \ + ln -sf /usr/local/bin/docker-wrapper.sh /usr/bin/docker # Set working directory WORKDIR /workspace diff --git a/docs/usage.md b/docs/usage.md index 43b81d9bd..83ab757f0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -25,6 +25,9 @@ Options: --env-all Pass all host environment variables to container -v, --mount Volume mount (host_path:container_path[:ro|rw]) --tty Allocate a pseudo-TTY for interactive tools + --build-local Build containers locally instead of using GHCR images + --agent-base-image Base image for agent container (requires --build-local) + See "Agent Base Image" section for available options -V, --version Output the version number -h, --help Display help for command @@ -359,6 +362,78 @@ SSL Bump requires intercepting HTTPS traffic: For more details, see [SSL Bump documentation](ssl-bump.md). +## Agent Base Image (GitHub Actions Parity) + +By default, the agent container uses `ubuntu:22.04`, a minimal image optimized for size (~200MB). When you need closer parity with GitHub Actions runner environments, you can specify an alternative base image. + +### Available Base Images + +| Image | Size | Description | +|-------|------|-------------| +| `ubuntu:22.04` (default) | ~200MB | Minimal Ubuntu, smallest footprint | +| `ghcr.io/catthehacker/ubuntu:runner-22.04` | ~2-5GB | Medium image with common tools, closer to GitHub Actions | +| `ghcr.io/catthehacker/ubuntu:full-22.04` | ~20GB compressed | Near-identical to GitHub Actions runner | + +### Usage + +The `--agent-base-image` flag requires `--build-local` since it customizes the container build: + +```bash +# Use runner image for better GitHub Actions compatibility +sudo awf \ + --build-local \ + --agent-base-image ghcr.io/catthehacker/ubuntu:runner-22.04 \ + --allow-domains github.com \ + -- your-command + +# Use full image for maximum parity (large download, ~20GB) +sudo awf \ + --build-local \ + --agent-base-image ghcr.io/catthehacker/ubuntu:full-22.04 \ + --allow-domains github.com \ + -- your-command +``` + +### When to Use Custom Base Images + +**Use `ubuntu:22.04` (default) when:** +- Fast startup time is important +- Minimal container size is preferred +- Your commands only need basic tools (curl, git, Node.js, Docker CLI) + +**Use `runner-22.04` when:** +- You need tools commonly available in GitHub Actions (multiple Python versions, Go, Java, etc.) +- Commands fail due to missing dependencies +- Moderate GitHub Actions parity is needed + +**Use `full-22.04` when:** +- Maximum GitHub Actions parity is required +- You need specific tools only available in the full runner image +- Download time and disk space are not concerns + +### Pre-installed Tools + +The default `ubuntu:22.04` image includes: +- Node.js 22 +- Docker CLI +- curl, git, iptables +- CA certificates +- Network utilities (dnsutils, net-tools, netcat) + +When using runner images, you get additional tools like: +- Multiple Python, Node.js, Go, Ruby versions +- Build tools (make, cmake, gcc) +- AWS CLI, Azure CLI, GitHub CLI +- Container tools (docker, buildx) +- And many more (see [catthehacker/docker_images](https://github.com/catthehacker/docker_images)) + +### Notes + +- Custom base images only work with `--build-local` (not GHCR images) +- First build with a new base image will take longer (downloading the image) +- Subsequent builds use Docker cache and are faster +- The `full-22.04` image requires significant disk space (~60GB extracted) + ## Limitations ### No Internationalized Domains diff --git a/src/cli.ts b/src/cli.ts index 81fa1f53b..ee621deda 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -344,6 +344,14 @@ program 'Build containers locally instead of using GHCR images', false ) + .option( + '--agent-base-image ', + 'Base image for agent container when using --build-local. Options:\n' + + ' ubuntu:22.04 (default): Minimal, ~200MB\n' + + ' ghcr.io/catthehacker/ubuntu:runner-22.04: Closer to GitHub Actions, ~2-5GB\n' + + ' ghcr.io/catthehacker/ubuntu:full-22.04: Near-identical to GitHub Actions, ~20GB', + 'ubuntu:22.04' + ) .option( '--image-registry ', 'Container image registry', @@ -611,6 +619,7 @@ program tty: options.tty || false, workDir: options.workDir, buildLocal: options.buildLocal, + agentBaseImage: options.agentBaseImage, imageRegistry: options.imageRegistry, imageTag: options.imageTag, additionalEnv: Object.keys(additionalEnv).length > 0 ? additionalEnv : undefined, @@ -624,6 +633,15 @@ program allowedUrls, }; + // Warn if using custom agent base image + if (options.agentBaseImage && options.agentBaseImage !== 'ubuntu:22.04') { + if (options.buildLocal) { + logger.info(`Using custom agent base image: ${options.agentBaseImage}`); + } else { + logger.warn('⚠️ --agent-base-image is only used with --build-local. Ignoring.'); + } + } + // Warn if --env-all is used if (config.envAll) { logger.warn('⚠️ Using --env-all: All host environment variables will be passed to container'); diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 179496a3c..9f5856c85 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -84,6 +84,27 @@ describe('docker-manager', () => { expect(result.services.agent.image).toBeUndefined(); }); + it('should pass BASE_IMAGE build arg when agentBaseImage is specified', () => { + const customBaseImageConfig = { + ...mockConfig, + buildLocal: true, + agentBaseImage: 'ghcr.io/catthehacker/ubuntu:runner-22.04', + }; + const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe('ghcr.io/catthehacker/ubuntu:runner-22.04'); + }); + + it('should not include BASE_IMAGE build arg when using default ubuntu:22.04', () => { + const localConfig = { ...mockConfig, buildLocal: true }; + const result = generateDockerCompose(localConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + // BASE_IMAGE should not be set when using the default (undefined or 'ubuntu:22.04') + expect(result.services.agent.build?.args?.BASE_IMAGE).toBeUndefined(); + }); + it('should use custom registry and tag', () => { const customConfig = { ...mockConfig, diff --git a/src/docker-manager.ts b/src/docker-manager.ts index e7eef8307..5dece1682 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -398,15 +398,22 @@ export function generateDockerCompose( if (useGHCR) { agentService.image = `${registry}/agent:${tag}`; } else { + const buildArgs: Record = { + // Pass host UID/GID to match file ownership in container + // This prevents permission issues with mounted volumes + USER_UID: getSafeHostUid(), + USER_GID: getSafeHostGid(), + }; + + // Allow custom base image for closer parity with GitHub Actions runner + if (config.agentBaseImage) { + buildArgs.BASE_IMAGE = config.agentBaseImage; + } + agentService.build = { context: path.join(projectRoot, 'containers/agent'), dockerfile: 'Dockerfile', - args: { - // Pass host UID/GID to match file ownership in container - // This prevents permission issues with mounted volumes - USER_UID: getSafeHostUid(), - USER_GID: getSafeHostGid(), - }, + args: buildArgs, }; } diff --git a/src/types.ts b/src/types.ts index eeff2c5f7..cac901356 100644 --- a/src/types.ts +++ b/src/types.ts @@ -147,6 +147,22 @@ export interface WrapperConfig { */ buildLocal?: boolean; + /** + * Base image for the agent container when building locally + * + * Allows customization of the agent container base image for closer parity + * with GitHub Actions runner environments. Only used when buildLocal is true. + * + * Options: + * - 'ubuntu:22.04' (default): Minimal image, smallest size (~200MB) + * - 'ghcr.io/catthehacker/ubuntu:runner-22.04': Closer to GitHub Actions runner (~2-5GB) + * - 'ghcr.io/catthehacker/ubuntu:full-22.04': Near-identical to GitHub Actions runner (~20GB compressed) + * + * @default 'ubuntu:22.04' + * @example 'ghcr.io/catthehacker/ubuntu:runner-22.04' + */ + agentBaseImage?: string; + /** * Additional environment variables to pass to the agent execution container * @@ -464,6 +480,8 @@ export interface DockerService { context: string; /** Path to the Dockerfile relative to context */ dockerfile: string; + /** Build arguments passed to docker build */ + args?: Record; }; /** From 6bf60c44c3dcca5f53f0125a0ff0b64aefd0e84e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:25:56 +0000 Subject: [PATCH 03/10] fix: address code review feedback for agent base image feature Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- containers/agent/Dockerfile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 62d8e4aa9..a90457e5f 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -21,8 +21,8 @@ RUN apt-get update && \ netcat-openbsd \ gosu \ libcap2-bin && \ - # Install Node.js 22 from NodeSource (only if not already installed or wrong version) - if ! command -v node >/dev/null 2>&1 || ! node --version | grep -q "v22\."; then \ + # Install Node.js 22 from NodeSource (only if not already installed or not v22.x) + if ! command -v node >/dev/null 2>&1 || ! node --version | grep -qE '^v22\.'; then \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs; \ fi && \ @@ -60,10 +60,15 @@ RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/ RUN if [ -f /usr/bin/docker ] && [ ! -L /usr/bin/docker ]; then \ mv /usr/bin/docker /usr/bin/docker-real; \ elif [ -L /usr/bin/docker ]; then \ - # Docker is a symlink, resolve and copy the target + # Docker is a symlink, resolve and copy the target (with error checking) DOCKER_TARGET=$(readlink -f /usr/bin/docker) && \ - rm /usr/bin/docker && \ - cp "$DOCKER_TARGET" /usr/bin/docker-real; \ + if [ -n "$DOCKER_TARGET" ] && [ -f "$DOCKER_TARGET" ]; then \ + rm /usr/bin/docker && \ + cp "$DOCKER_TARGET" /usr/bin/docker-real; \ + else \ + # Broken symlink or target doesn't exist, just remove it + rm /usr/bin/docker; \ + fi; \ fi && \ ln -sf /usr/local/bin/docker-wrapper.sh /usr/bin/docker From 39594c94cb59aee2badbff34e6031782b5231c03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:45:49 +0000 Subject: [PATCH 04/10] docs: add security considerations for custom base images Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- docs/usage.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 83ab757f0..2af8dc222 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -411,6 +411,30 @@ sudo awf \ - You need specific tools only available in the full runner image - Download time and disk space are not concerns +### Security Considerations + +**⚠️ IMPORTANT:** Custom base images introduce supply chain risk. When using third-party images: + +1. **Verify image sources** - Only use images from trusted publishers. The `catthehacker` images are community-maintained and not officially supported by GitHub. + +2. **Review image contents** - Understand what tools and configurations are included. Third-party images may contain pre-installed software that could behave unexpectedly. + +3. **Pin specific versions** - Use image digests (e.g., `@sha256:...`) instead of mutable tags to prevent tag manipulation: + ```bash + --agent-base-image ghcr.io/catthehacker/ubuntu@sha256:abc123... + ``` + +4. **Monitor for vulnerabilities** - Third-party images may not receive timely security updates compared to official images. + +**Existing security controls remain in effect:** +- Host-level iptables (DOCKER-USER chain) enforce egress filtering regardless of container contents +- Squid proxy enforces domain allowlist at L7 +- NET_ADMIN capability is dropped before user command execution +- Seccomp profile blocks dangerous syscalls +- `no-new-privileges` prevents privilege escalation + +**For maximum security, use the default `ubuntu:22.04` image.** Custom base images are recommended only when you trust the image publisher and the benefits outweigh the supply chain risks. + ### Pre-installed Tools The default `ubuntu:22.04` image includes: From 93efcd037882156830f91a66d51b32cb8d637ab9 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Sat, 17 Jan 2026 04:18:06 +0000 Subject: [PATCH 05/10] fix: improve docker wrapper for non-standard paths The previous implementation only checked /usr/bin/docker specifically, which could fail when docker is installed in other locations like /usr/local/bin/docker (common in runner images). This caused the wrapper to create a symlink at /usr/bin/docker but fail to create docker-real, breaking the wrapper functionality. Changes: - Use 'command -v docker' to find docker regardless of installation path - Copy docker binary to docker-real from any location - Only remove /usr/bin/docker if that's where docker was found - Add explicit error handling for broken symlinks This ensures the wrapper works correctly with both minimal ubuntu:22.04 and GitHub Actions runner images that may have docker in different paths. Co-Authored-By: Claude Sonnet 4.5 --- containers/agent/Dockerfile | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index a90457e5f..a244ab89e 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -57,16 +57,23 @@ RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/ # Install docker wrapper to intercept docker commands # Handle cases where docker may already exist (symlink or binary) -RUN if [ -f /usr/bin/docker ] && [ ! -L /usr/bin/docker ]; then \ - mv /usr/bin/docker /usr/bin/docker-real; \ - elif [ -L /usr/bin/docker ]; then \ - # Docker is a symlink, resolve and copy the target (with error checking) - DOCKER_TARGET=$(readlink -f /usr/bin/docker) && \ - if [ -n "$DOCKER_TARGET" ] && [ -f "$DOCKER_TARGET" ]; then \ - rm /usr/bin/docker && \ - cp "$DOCKER_TARGET" /usr/bin/docker-real; \ +RUN DOCKER_PATH=$(command -v docker || echo "") && \ + if [ -n "$DOCKER_PATH" ]; then \ + # Docker exists somewhere in PATH + if [ -L "$DOCKER_PATH" ]; then \ + # Docker is a symlink, resolve and copy the target (with error checking) + DOCKER_TARGET=$(readlink -f "$DOCKER_PATH") && \ + if [ -n "$DOCKER_TARGET" ] && [ -f "$DOCKER_TARGET" ]; then \ + cp "$DOCKER_TARGET" /usr/bin/docker-real; \ + else \ + echo "ERROR: Docker symlink at $DOCKER_PATH is broken" && exit 1; \ + fi; \ else \ - # Broken symlink or target doesn't exist, just remove it + # Docker is a regular file, copy it to docker-real + cp "$DOCKER_PATH" /usr/bin/docker-real; \ + fi; \ + # Remove existing docker if it's at /usr/bin/docker + if [ "$DOCKER_PATH" = "/usr/bin/docker" ]; then \ rm /usr/bin/docker; \ fi; \ fi && \ From 7b856e7e832e1d36ff59976e931b195f8b6d231e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:22:57 +0000 Subject: [PATCH 06/10] feat: add input validation for agent base image Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- src/cli.test.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++++++- src/cli.ts | 51 ++++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 5f2248c87..27dbbb109 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers } from './cli'; +import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentBaseImage } from './cli'; import { redactSecrets } from './redact-secrets'; import * as fs from 'fs'; import * as path from 'path'; @@ -774,4 +774,74 @@ describe('cli', () => { expect(DEFAULT_DNS_SERVERS).toEqual(['8.8.8.8', '8.8.4.4']); }); }); + + describe('validateAgentBaseImage', () => { + describe('valid images', () => { + it('should accept official Ubuntu images', () => { + expect(validateAgentBaseImage('ubuntu:22.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ubuntu:24.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ubuntu:20.04')).toEqual({ valid: true }); + }); + + it('should accept catthehacker runner images', () => { + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-22.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-24.04')).toEqual({ valid: true }); + }); + + it('should accept catthehacker full images', () => { + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-22.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-24.04')).toEqual({ valid: true }); + }); + + it('should accept images with SHA256 digest pinning', () => { + expect(validateAgentBaseImage('ubuntu:22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + }); + }); + + describe('invalid images', () => { + it('should reject arbitrary images', () => { + const result = validateAgentBaseImage('malicious-registry.com/evil:latest'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject images with typos', () => { + const result = validateAgentBaseImage('ubunto:22.04'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject non-ubuntu official images', () => { + const result = validateAgentBaseImage('alpine:latest'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject unknown registries', () => { + const result = validateAgentBaseImage('docker.io/library/ubuntu:22.04'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject images from other catthehacker registries', () => { + const result = validateAgentBaseImage('ghcr.io/catthehacker/debian:latest'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject ubuntu with non-standard tags', () => { + const result = validateAgentBaseImage('ubuntu:latest'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject empty image string', () => { + const result = validateAgentBaseImage(''); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + }); + }); }); diff --git a/src/cli.ts b/src/cli.ts index f9d3678b7..ada9b3cfa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -101,6 +101,48 @@ export function isValidIPv6(ip: string): boolean { return isIPv6(ip); } +/** + * Safe patterns for agent base images to prevent supply chain attacks. + * Allows: + * - Official Ubuntu images (ubuntu:XX.XX) + * - catthehacker runner images (ghcr.io/catthehacker/ubuntu:runner-XX.XX or full-XX.XX) + * - Images with SHA256 digest pinning + */ +const SAFE_BASE_IMAGE_PATTERNS = [ + // Official Ubuntu images (e.g., ubuntu:22.04, ubuntu:24.04) + /^ubuntu:\d+\.\d+$/, + // catthehacker runner images (e.g., ghcr.io/catthehacker/ubuntu:runner-22.04) + /^ghcr\.io\/catthehacker\/ubuntu:(runner|full)-\d+\.\d+$/, + // catthehacker images with SHA256 digest pinning + /^ghcr\.io\/catthehacker\/ubuntu:(runner|full)-\d+\.\d+@sha256:[a-f0-9]{64}$/, + // Official Ubuntu images with SHA256 digest pinning + /^ubuntu:\d+\.\d+@sha256:[a-f0-9]{64}$/, +]; + +/** + * Validates that a base image is from an approved source to prevent supply chain attacks. + * @param image - Docker image reference to validate + * @returns Object with valid boolean and optional error message + */ +export function validateAgentBaseImage(image: string): { valid: boolean; error?: string } { + // Check against safe patterns + const isValid = SAFE_BASE_IMAGE_PATTERNS.some(pattern => pattern.test(image)); + + if (isValid) { + return { valid: true }; + } + + return { + valid: false, + error: `Invalid base image: "${image}". ` + + 'For security, only approved base images are allowed:\n' + + ' - ubuntu:XX.XX (e.g., ubuntu:22.04)\n' + + ' - ghcr.io/catthehacker/ubuntu:runner-XX.XX\n' + + ' - ghcr.io/catthehacker/ubuntu:full-XX.XX\n' + + 'Use @sha256:... suffix for digest-pinned versions.' + }; +} + /** * Parses and validates DNS servers from a comma-separated string * @param input - Comma-separated DNS server string (e.g., "8.8.8.8,1.1.1.1") @@ -640,8 +682,15 @@ program allowedUrls, }; - // Warn if using custom agent base image + // Validate and warn if using custom agent base image if (options.agentBaseImage && options.agentBaseImage !== 'ubuntu:22.04') { + // Validate against approved base images for supply chain security + const validation = validateAgentBaseImage(options.agentBaseImage); + if (!validation.valid) { + logger.error(validation.error!); + process.exit(1); + } + if (options.buildLocal) { logger.info(`Using custom agent base image: ${options.agentBaseImage}`); } else { From 8976ec43872c441bc2f1f7159832e56adf4dbf50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:41:54 +0000 Subject: [PATCH 07/10] test: add more edge case tests for agent base image validation Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- src/cli.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/cli.test.ts b/src/cli.test.ts index 27dbbb109..612ef5dcf 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -842,6 +842,39 @@ describe('cli', () => { expect(result.valid).toBe(false); expect(result.error).toContain('Invalid base image'); }); + + it('should reject ubuntu with only major version', () => { + const result = validateAgentBaseImage('ubuntu:22'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject catthehacker with wrong prefix', () => { + const result = validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:minimal-22.04'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject malformed SHA256 digest (too short)', () => { + const result = validateAgentBaseImage('ubuntu:22.04@sha256:abc123'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should reject image with path traversal attempt', () => { + const result = validateAgentBaseImage('../ubuntu:22.04'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid base image'); + }); + + it('should provide helpful error message with allowed options', () => { + const result = validateAgentBaseImage('invalid:image'); + expect(result.valid).toBe(false); + expect(result.error).toContain('ubuntu:XX.XX'); + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:runner-XX.XX'); + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:full-XX.XX'); + expect(result.error).toContain('@sha256:'); + }); }); }); }); From c87c634c76aaef0beca9aa3a4cda60fabce60e70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:51:00 +0000 Subject: [PATCH 08/10] test: increase coverage with additional validation tests Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- src/cli.test.ts | 33 +++++++++++++++++++++++++++++++++ src/docker-manager.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/cli.test.ts b/src/cli.test.ts index 612ef5dcf..6cf68dbf6 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -876,5 +876,38 @@ describe('cli', () => { expect(result.error).toContain('@sha256:'); }); }); + + describe('regex pattern coverage', () => { + // Ensure each regex pattern in SAFE_BASE_IMAGE_PATTERNS is individually tested + it('should match pattern 1: plain ubuntu version', () => { + expect(validateAgentBaseImage('ubuntu:18.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ubuntu:26.10')).toEqual({ valid: true }); + }); + + it('should match pattern 2: catthehacker runner/full without digest', () => { + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-18.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-26.10')).toEqual({ valid: true }); + }); + + it('should match pattern 3: catthehacker with SHA256 digest', () => { + const digest = 'sha256:' + '1234567890abcdef'.repeat(4); + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:runner-22.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:full-24.04@${digest}`)).toEqual({ valid: true }); + }); + + it('should match pattern 4: plain ubuntu with SHA256 digest', () => { + const digest = 'sha256:' + 'abcdef1234567890'.repeat(4); + expect(validateAgentBaseImage(`ubuntu:22.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentBaseImage(`ubuntu:24.04@${digest}`)).toEqual({ valid: true }); + }); + + it('should reject images that almost match but do not exactly', () => { + // Nearly matching but invalid + expect(validateAgentBaseImage('ubuntu:22.04 ').valid).toBe(false); // trailing space + expect(validateAgentBaseImage(' ubuntu:22.04').valid).toBe(false); // leading space + expect(validateAgentBaseImage('Ubuntu:22.04').valid).toBe(false); // capital U + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:Runner-22.04').valid).toBe(false); // capital R + }); + }); }); }); diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index d01afb96b..65fd10070 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -105,6 +105,31 @@ describe('docker-manager', () => { expect(result.services.agent.build?.args?.BASE_IMAGE).toBeUndefined(); }); + it('should pass BASE_IMAGE build arg when agentBaseImage with SHA256 digest is specified', () => { + const customBaseImageConfig = { + ...mockConfig, + buildLocal: true, + agentBaseImage: 'ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1', + }; + const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe('ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1'); + }); + + it('should not pass BASE_IMAGE when agentBaseImage is explicitly set to default ubuntu:22.04', () => { + const customBaseImageConfig = { + ...mockConfig, + buildLocal: true, + agentBaseImage: 'ubuntu:22.04', + }; + const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + // The code only sets BASE_IMAGE if agentBaseImage is defined (truthy), so ubuntu:22.04 would be set + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe('ubuntu:22.04'); + }); + it('should use custom registry and tag', () => { const customConfig = { ...mockConfig, From 0734c880e7c315af4995897560c07321f0195be3 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Thu, 22 Jan 2026 23:59:05 +0000 Subject: [PATCH 09/10] feat: add gh CLI, robust user creation, and act image support - Add gh (GitHub CLI) package to agent container - Add PATH manipulation to prefer system binaries over toolcache - Make user/group creation idempotent (check if exists first) - Add catthehacker act-XX.XX images to allowlist Co-Authored-By: Claude Opus 4.5 --- containers/agent/Dockerfile | 19 +++++++++++++++++-- src/cli.ts | 9 +++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 0cab397f0..1a9c16dd1 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -15,12 +15,15 @@ RUN apt-get update && \ curl \ ca-certificates \ git \ + gh \ gnupg \ dnsutils \ net-tools \ netcat-openbsd \ gosu \ libcap2-bin && \ + # Prefer system binaries over runner toolcache (e.g., act images) for Node checks. + export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" && \ # Install Node.js 22 from NodeSource # Check if Node.js 22 is already installed (common in runner images) if ! command -v node >/dev/null 2>&1 || ! node --version | grep -qE '^v22\.'; then \ @@ -39,8 +42,20 @@ RUN apt-get update && \ # and prevents file ownership issues with mounted volumes ARG USER_UID=1000 ARG USER_GID=1000 -RUN groupadd -g ${USER_GID} awfuser && \ - useradd -u ${USER_UID} -g ${USER_GID} -m -s /bin/bash awfuser && \ +RUN if ! getent group awfuser >/dev/null 2>&1; then \ + if ! getent group ${USER_GID} >/dev/null 2>&1; then \ + groupadd -g ${USER_GID} awfuser; \ + else \ + groupadd awfuser; \ + fi; \ + fi && \ + if ! id -u awfuser >/dev/null 2>&1; then \ + if ! getent passwd ${USER_UID} >/dev/null 2>&1; then \ + useradd -u ${USER_UID} -g awfuser -m -s /bin/bash awfuser; \ + else \ + useradd -g awfuser -m -s /bin/bash awfuser; \ + fi; \ + fi && \ # Create directories for awfuser mkdir -p /home/awfuser/.copilot/logs && \ chown -R awfuser:awfuser /home/awfuser diff --git a/src/cli.ts b/src/cli.ts index ada9b3cfa..caf480d7c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -105,16 +105,16 @@ export function isValidIPv6(ip: string): boolean { * Safe patterns for agent base images to prevent supply chain attacks. * Allows: * - Official Ubuntu images (ubuntu:XX.XX) - * - catthehacker runner images (ghcr.io/catthehacker/ubuntu:runner-XX.XX or full-XX.XX) + * - catthehacker runner images (ghcr.io/catthehacker/ubuntu:runner-XX.XX, full-XX.XX, or act-XX.XX) * - Images with SHA256 digest pinning */ const SAFE_BASE_IMAGE_PATTERNS = [ // Official Ubuntu images (e.g., ubuntu:22.04, ubuntu:24.04) /^ubuntu:\d+\.\d+$/, - // catthehacker runner images (e.g., ghcr.io/catthehacker/ubuntu:runner-22.04) - /^ghcr\.io\/catthehacker\/ubuntu:(runner|full)-\d+\.\d+$/, + // catthehacker runner images (e.g., ghcr.io/catthehacker/ubuntu:runner-22.04, act-24.04) + /^ghcr\.io\/catthehacker\/ubuntu:(runner|full|act)-\d+\.\d+$/, // catthehacker images with SHA256 digest pinning - /^ghcr\.io\/catthehacker\/ubuntu:(runner|full)-\d+\.\d+@sha256:[a-f0-9]{64}$/, + /^ghcr\.io\/catthehacker\/ubuntu:(runner|full|act)-\d+\.\d+@sha256:[a-f0-9]{64}$/, // Official Ubuntu images with SHA256 digest pinning /^ubuntu:\d+\.\d+@sha256:[a-f0-9]{64}$/, ]; @@ -139,6 +139,7 @@ export function validateAgentBaseImage(image: string): { valid: boolean; error?: ' - ubuntu:XX.XX (e.g., ubuntu:22.04)\n' + ' - ghcr.io/catthehacker/ubuntu:runner-XX.XX\n' + ' - ghcr.io/catthehacker/ubuntu:full-XX.XX\n' + + ' - ghcr.io/catthehacker/ubuntu:act-XX.XX\n' + 'Use @sha256:... suffix for digest-pinned versions.' }; } From f5f63757bf191919ab567ec3b687cfea4d0e35c7 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Fri, 23 Jan 2026 00:05:00 +0000 Subject: [PATCH 10/10] test: add tests for catthehacker act image variant Cover the act image type in validation tests to fix coverage regression. Co-Authored-By: Claude Opus 4.5 --- src/cli.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 6cf68dbf6..828ecee44 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -793,10 +793,16 @@ describe('cli', () => { expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-24.04')).toEqual({ valid: true }); }); + it('should accept catthehacker act images', () => { + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-24.04')).toEqual({ valid: true }); + }); + it('should accept images with SHA256 digest pinning', () => { expect(validateAgentBaseImage('ubuntu:22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); }); }); @@ -873,6 +879,7 @@ describe('cli', () => { expect(result.error).toContain('ubuntu:XX.XX'); expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:runner-XX.XX'); expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:full-XX.XX'); + expect(result.error).toContain('ghcr.io/catthehacker/ubuntu:act-XX.XX'); expect(result.error).toContain('@sha256:'); }); }); @@ -884,15 +891,17 @@ describe('cli', () => { expect(validateAgentBaseImage('ubuntu:26.10')).toEqual({ valid: true }); }); - it('should match pattern 2: catthehacker runner/full without digest', () => { + it('should match pattern 2: catthehacker runner/full/act without digest', () => { expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:runner-18.04')).toEqual({ valid: true }); expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:full-26.10')).toEqual({ valid: true }); + expect(validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:act-22.04')).toEqual({ valid: true }); }); it('should match pattern 3: catthehacker with SHA256 digest', () => { const digest = 'sha256:' + '1234567890abcdef'.repeat(4); expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:runner-22.04@${digest}`)).toEqual({ valid: true }); expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:full-24.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentBaseImage(`ghcr.io/catthehacker/ubuntu:act-22.04@${digest}`)).toEqual({ valid: true }); }); it('should match pattern 4: plain ubuntu with SHA256 digest', () => {