diff --git a/docs/usage.md b/docs/usage.md index bca28fc5..66f62518 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -26,8 +26,8 @@ Options: -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 + --agent-image Agent container image (default: "default") + See "Agent Image" section for available options -V, --version Output the version number -h, --help Display help for command @@ -345,54 +345,76 @@ SSL Bump requires intercepting HTTPS traffic: For more details, see [SSL Bump documentation](ssl-bump.md). -## Agent Base Image (GitHub Actions Parity) +## Agent Image -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. +The `--agent-image` flag controls which agent container image to use. It supports two presets for quick startup, or custom base images for advanced use cases. -### Available Base Images +### Presets (Pre-built, Fast Startup) -| 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 | +| Preset | GHCR Image | Base | Size | Use Case | +|--------|------------|------|------|----------| +| `default` | `agent:latest` | `ubuntu:22.04` | ~200MB | Minimal, fast startup | +| `act` | `agent-act:latest` | `catthehacker/ubuntu:act-24.04` | ~2GB | GitHub Actions parity | + +```bash +# Use default preset (minimal image, fastest startup) +sudo awf --allow-domains github.com -- your-command + +# Explicitly specify default +sudo awf --agent-image default --allow-domains github.com -- your-command + +# Use act preset for GitHub Actions parity +sudo awf --agent-image act --allow-domains github.com -- your-command +``` -### Usage +### Custom Base Images (Requires --build-local) -The `--agent-base-image` flag requires `--build-local` since it customizes the container build: +For advanced use cases, you can specify a custom base image. This requires `--build-local` since it customizes the container build: + +| Image | Size | Description | +|-------|------|-------------| +| `ubuntu:XX.XX` | ~200MB | Official Ubuntu image | +| `ghcr.io/catthehacker/ubuntu:runner-XX.XX` | ~2-5GB | Medium image with common tools | +| `ghcr.io/catthehacker/ubuntu:full-XX.XX` | ~20GB | Near-identical to GitHub Actions runner | ```bash -# Use runner image for better GitHub Actions compatibility +# Use custom runner image (requires --build-local) sudo awf \ --build-local \ - --agent-base-image ghcr.io/catthehacker/ubuntu:runner-22.04 \ + --agent-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 \ + --agent-image ghcr.io/catthehacker/ubuntu:full-22.04 \ --allow-domains github.com \ -- your-command ``` -### When to Use Custom Base Images +**Error handling:** Using a custom image without `--build-local` will result in an error: +``` +❌ Custom agent images require --build-local flag + Example: awf --build-local --agent-image ghcr.io/catthehacker/ubuntu:runner-22.04 ... +``` + +### When to Use Each Option -**Use `ubuntu:22.04` (default) when:** +**Use `default` preset 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 `act` preset when:** +- You need GitHub Actions parity without building locally +- Fast startup is still important +- You trust the pre-built GHCR image -**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 +**Use custom base images with `--build-local` when:** +- You need specific runner variants (runner-22.04, full-22.04) +- You want to pin to a specific digest for reproducibility +- You need maximum control over the base image ### Security Considerations @@ -404,7 +426,7 @@ sudo awf \ 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... + --agent-image ghcr.io/catthehacker/ubuntu:runner-22.04@sha256:abc123... ``` 4. **Monitor for vulnerabilities** - Third-party images may not receive timely security updates compared to official images. @@ -416,7 +438,7 @@ sudo awf \ - 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. +**For maximum security, use the `default` preset.** Custom base images are recommended only when you trust the image publisher and the benefits outweigh the supply chain risks. ### Pre-installed Tools @@ -427,7 +449,7 @@ The default `ubuntu:22.04` image includes: - CA certificates - Network utilities (dnsutils, net-tools, netcat) -When using runner images, you get additional tools like: +When using runner/full images or the `act` preset, you get additional tools like: - Multiple Python, Node.js, Go, Ruby versions - Build tools (make, cmake, gcc) - AWS CLI, Azure CLI, GitHub CLI @@ -436,10 +458,11 @@ When using runner images, you get additional tools like: ### Notes -- Custom base images only work with `--build-local` (not GHCR images) +- Presets (`default`, `act`) use pre-built GHCR images for fast startup +- Custom base images require `--build-local` and build time on first use - 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) +- The `full-XX.XX` images require significant disk space (~60GB extracted) ## Limitations diff --git a/src/cli.test.ts b/src/cli.test.ts index 828ecee4..ea697c90 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, validateAgentBaseImage } from './cli'; +import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption } from './cli'; import { redactSecrets } from './redact-secrets'; import * as fs from 'fs'; import * as path from 'path'; @@ -775,107 +775,182 @@ describe('cli', () => { }); }); - describe('validateAgentBaseImage', () => { - describe('valid images', () => { + describe('isAgentImagePreset', () => { + it('should return true for "default" preset', () => { + expect(isAgentImagePreset('default')).toBe(true); + }); + + it('should return true for "act" preset', () => { + expect(isAgentImagePreset('act')).toBe(true); + }); + + it('should return false for custom images', () => { + expect(isAgentImagePreset('ubuntu:22.04')).toBe(false); + expect(isAgentImagePreset('ghcr.io/catthehacker/ubuntu:runner-22.04')).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isAgentImagePreset(undefined)).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isAgentImagePreset('')).toBe(false); + }); + + it('should return false for case variations of presets', () => { + expect(isAgentImagePreset('Default')).toBe(false); + expect(isAgentImagePreset('DEFAULT')).toBe(false); + expect(isAgentImagePreset('Act')).toBe(false); + expect(isAgentImagePreset('ACT')).toBe(false); + }); + + it('should return false for presets with whitespace', () => { + expect(isAgentImagePreset(' default')).toBe(false); + expect(isAgentImagePreset('default ')).toBe(false); + expect(isAgentImagePreset(' act ')).toBe(false); + }); + + it('should return false for similar but not exact preset names', () => { + expect(isAgentImagePreset('defaults')).toBe(false); + expect(isAgentImagePreset('action')).toBe(false); + expect(isAgentImagePreset('def')).toBe(false); + }); + }); + + describe('AGENT_IMAGE_PRESETS', () => { + it('should contain default and act', () => { + expect(AGENT_IMAGE_PRESETS).toContain('default'); + expect(AGENT_IMAGE_PRESETS).toContain('act'); + expect(AGENT_IMAGE_PRESETS.length).toBe(2); + }); + }); + + describe('validateAgentImage', () => { + describe('presets', () => { + it('should accept "default" preset', () => { + expect(validateAgentImage('default')).toEqual({ valid: true }); + }); + + it('should accept "act" preset', () => { + expect(validateAgentImage('act')).toEqual({ valid: true }); + }); + }); + + describe('valid custom 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 }); + expect(validateAgentImage('ubuntu:22.04')).toEqual({ valid: true }); + expect(validateAgentImage('ubuntu:24.04')).toEqual({ valid: true }); + expect(validateAgentImage('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 }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:runner-22.04')).toEqual({ valid: true }); + expect(validateAgentImage('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 }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:full-22.04')).toEqual({ valid: true }); + expect(validateAgentImage('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 }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:act-22.04')).toEqual({ valid: true }); + expect(validateAgentImage('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 }); + expect(validateAgentImage('ubuntu:22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:runner-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:act-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1')).toEqual({ valid: true }); }); }); - describe('invalid images', () => { + describe('invalid custom images', () => { it('should reject arbitrary images', () => { - const result = validateAgentBaseImage('malicious-registry.com/evil:latest'); + const result = validateAgentImage('malicious-registry.com/evil:latest'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject images with typos', () => { - const result = validateAgentBaseImage('ubunto:22.04'); + const result = validateAgentImage('ubunto:22.04'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject non-ubuntu official images', () => { - const result = validateAgentBaseImage('alpine:latest'); + const result = validateAgentImage('alpine:latest'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject unknown registries', () => { - const result = validateAgentBaseImage('docker.io/library/ubuntu:22.04'); + const result = validateAgentImage('docker.io/library/ubuntu:22.04'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject images from other catthehacker registries', () => { - const result = validateAgentBaseImage('ghcr.io/catthehacker/debian:latest'); + const result = validateAgentImage('ghcr.io/catthehacker/debian:latest'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject ubuntu with non-standard tags', () => { - const result = validateAgentBaseImage('ubuntu:latest'); + const result = validateAgentImage('ubuntu:latest'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject empty image string', () => { - const result = validateAgentBaseImage(''); + const result = validateAgentImage(''); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject ubuntu with only major version', () => { - const result = validateAgentBaseImage('ubuntu:22'); + const result = validateAgentImage('ubuntu:22'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject catthehacker with wrong prefix', () => { - const result = validateAgentBaseImage('ghcr.io/catthehacker/ubuntu:minimal-22.04'); + const result = validateAgentImage('ghcr.io/catthehacker/ubuntu:minimal-22.04'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); it('should reject malformed SHA256 digest (too short)', () => { - const result = validateAgentBaseImage('ubuntu:22.04@sha256:abc123'); + const result = validateAgentImage('ubuntu:22.04@sha256:abc123'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); + }); + + it('should reject SHA256 digest with uppercase hex', () => { + const result = validateAgentImage('ubuntu:22.04@sha256:A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid agent image'); }); it('should reject image with path traversal attempt', () => { - const result = validateAgentBaseImage('../ubuntu:22.04'); + const result = validateAgentImage('../ubuntu:22.04'); expect(result.valid).toBe(false); - expect(result.error).toContain('Invalid base image'); + expect(result.error).toContain('Invalid agent image'); }); - it('should provide helpful error message with allowed options', () => { - const result = validateAgentBaseImage('invalid:image'); + it('should reject similar but invalid registry paths', () => { + // Similar to ghcr.io/catthehacker but different + expect(validateAgentImage('ghcr.io/catthehacker2/ubuntu:runner-22.04').valid).toBe(false); + expect(validateAgentImage('ghcr.io/catthehackerubuntu:runner-22.04').valid).toBe(false); + expect(validateAgentImage('ghcr.io/cat-the-hacker/ubuntu:runner-22.04').valid).toBe(false); + }); + + it('should provide helpful error message with allowed options including presets', () => { + const result = validateAgentImage('invalid:image'); expect(result.valid).toBe(false); + expect(result.error).toContain('default'); + expect(result.error).toContain('act'); 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'); @@ -887,35 +962,181 @@ describe('cli', () => { 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 }); + expect(validateAgentImage('ubuntu:18.04')).toEqual({ valid: true }); + expect(validateAgentImage('ubuntu:26.10')).toEqual({ valid: true }); }); 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 }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:runner-18.04')).toEqual({ valid: true }); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:full-26.10')).toEqual({ valid: true }); + expect(validateAgentImage('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 }); + expect(validateAgentImage(`ghcr.io/catthehacker/ubuntu:runner-22.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentImage(`ghcr.io/catthehacker/ubuntu:full-24.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentImage(`ghcr.io/catthehacker/ubuntu:act-22.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 }); + expect(validateAgentImage(`ubuntu:22.04@${digest}`)).toEqual({ valid: true }); + expect(validateAgentImage(`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 + expect(validateAgentImage('ubuntu:22.04 ').valid).toBe(false); // trailing space + expect(validateAgentImage(' ubuntu:22.04').valid).toBe(false); // leading space + expect(validateAgentImage('Ubuntu:22.04').valid).toBe(false); // capital U + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:Runner-22.04').valid).toBe(false); // capital R + }); + }); + + describe('edge cases', () => { + it('should handle special characters in image names', () => { + expect(validateAgentImage('ubuntu:22.04;rm -rf /').valid).toBe(false); + expect(validateAgentImage('ubuntu:22.04 && malicious').valid).toBe(false); + expect(validateAgentImage('ubuntu:22.04|cat /etc/passwd').valid).toBe(false); + expect(validateAgentImage('ubuntu:22.04`whoami`').valid).toBe(false); + }); + + it('should reject newlines and control characters', () => { + expect(validateAgentImage('ubuntu:22.04\nmalicious').valid).toBe(false); + expect(validateAgentImage('ubuntu:22.04\tmalicious').valid).toBe(false); + expect(validateAgentImage('ubuntu:22.04\rmalicious').valid).toBe(false); + }); + + it('should reject URL-like injection attempts', () => { + expect(validateAgentImage('http://evil.com/ubuntu:22.04').valid).toBe(false); + expect(validateAgentImage('https://evil.com/image').valid).toBe(false); + }); + + it('should reject environment variable injection', () => { + expect(validateAgentImage('ubuntu:$VERSION').valid).toBe(false); + expect(validateAgentImage('ubuntu:${VERSION}').valid).toBe(false); + }); + + it('should reject images with multiple @ symbols', () => { + expect(validateAgentImage('ubuntu:22.04@sha256:abc@sha256:def').valid).toBe(false); + }); + + it('should reject catthehacker with extra path segments', () => { + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu/extra:runner-22.04').valid).toBe(false); + expect(validateAgentImage('ghcr.io/catthehacker/ubuntu:runner-22.04/extra').valid).toBe(false); + }); + + it('should accept valid edge case versions', () => { + // High version numbers + expect(validateAgentImage('ubuntu:99.99')).toEqual({ valid: true }); + // Single digit versions + expect(validateAgentImage('ubuntu:1.04')).toEqual({ valid: true }); + }); + }); + }); + + describe('processAgentImageOption', () => { + describe('default preset', () => { + it('should return default when no option provided', () => { + const result = processAgentImageOption(undefined, false); + expect(result.agentImage).toBe('default'); + expect(result.isPreset).toBe(true); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBeUndefined(); + }); + + it('should return default when explicitly set', () => { + const result = processAgentImageOption('default', false); + expect(result.agentImage).toBe('default'); + expect(result.isPreset).toBe(true); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBeUndefined(); + }); + + it('should work with --build-local', () => { + const result = processAgentImageOption('default', true); + expect(result.agentImage).toBe('default'); + expect(result.isPreset).toBe(true); + expect(result.error).toBeUndefined(); + }); + }); + + describe('act preset', () => { + it('should return act preset with info message', () => { + const result = processAgentImageOption('act', false); + expect(result.agentImage).toBe('act'); + expect(result.isPreset).toBe(true); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBe('Using agent image preset: act (GitHub Actions parity)'); + }); + + it('should work with --build-local', () => { + const result = processAgentImageOption('act', true); + expect(result.agentImage).toBe('act'); + expect(result.isPreset).toBe(true); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBe('Using agent image preset: act (GitHub Actions parity)'); + }); + }); + + describe('custom images', () => { + it('should require --build-local for custom images', () => { + const result = processAgentImageOption('ubuntu:22.04', false); + expect(result.agentImage).toBe('ubuntu:22.04'); + expect(result.isPreset).toBe(false); + expect(result.requiresBuildLocal).toBe(true); + expect(result.error).toContain('Custom agent images require --build-local flag'); + }); + + it('should accept custom ubuntu image with --build-local', () => { + const result = processAgentImageOption('ubuntu:22.04', true); + expect(result.agentImage).toBe('ubuntu:22.04'); + expect(result.isPreset).toBe(false); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBe('Using custom agent base image: ubuntu:22.04'); + }); + + it('should accept catthehacker runner image with --build-local', () => { + const result = processAgentImageOption('ghcr.io/catthehacker/ubuntu:runner-22.04', true); + expect(result.agentImage).toBe('ghcr.io/catthehacker/ubuntu:runner-22.04'); + expect(result.isPreset).toBe(false); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toBe('Using custom agent base image: ghcr.io/catthehacker/ubuntu:runner-22.04'); + }); + + it('should accept catthehacker full image with --build-local', () => { + const result = processAgentImageOption('ghcr.io/catthehacker/ubuntu:full-24.04', true); + expect(result.agentImage).toBe('ghcr.io/catthehacker/ubuntu:full-24.04'); + expect(result.isPreset).toBe(false); + expect(result.error).toBeUndefined(); + expect(result.infoMessage).toContain('full-24.04'); + }); + + it('should accept image with SHA256 digest with --build-local', () => { + const image = 'ubuntu:22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1'; + const result = processAgentImageOption(image, true); + expect(result.agentImage).toBe(image); + expect(result.isPreset).toBe(false); + expect(result.error).toBeUndefined(); + }); + }); + + describe('invalid images', () => { + it('should return error for invalid image', () => { + const result = processAgentImageOption('malicious:image', false); + expect(result.error).toContain('Invalid agent image'); + expect(result.isPreset).toBe(false); + }); + + it('should return error for invalid image even with --build-local', () => { + const result = processAgentImageOption('malicious:image', true); + expect(result.error).toContain('Invalid agent image'); + }); + + it('should return error for alpine image', () => { + const result = processAgentImageOption('alpine:latest', true); + expect(result.error).toContain('Invalid agent image'); }); }); }); diff --git a/src/cli.ts b/src/cli.ts index caf480d7..ce86006d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -102,7 +102,12 @@ export function isValidIPv6(ip: string): boolean { } /** - * Safe patterns for agent base images to prevent supply chain attacks. + * Pre-defined agent image presets + */ +export const AGENT_IMAGE_PRESETS = ['default', 'act'] as const; + +/** + * Safe patterns for custom 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, full-XX.XX, or act-XX.XX) @@ -120,12 +125,26 @@ const SAFE_BASE_IMAGE_PATTERNS = [ ]; /** - * Validates that a base image is from an approved source to prevent supply chain attacks. - * @param image - Docker image reference to validate + * Checks if the given value is a preset name (default, act) + */ +export function isAgentImagePreset(value: string | undefined): value is 'default' | 'act' { + return value === 'default' || value === 'act'; +} + +/** + * Validates that an agent image value is either a preset or an approved custom base image. + * For presets ('default', 'act'), validation always passes. + * For custom images, validates against approved patterns to prevent supply chain attacks. + * @param image - Agent image value (preset or custom image reference) * @returns Object with valid boolean and optional error message */ -export function validateAgentBaseImage(image: string): { valid: boolean; error?: string } { - // Check against safe patterns +export function validateAgentImage(image: string): { valid: boolean; error?: string } { + // Presets are always valid + if (isAgentImagePreset(image)) { + return { valid: true }; + } + + // Check custom images against safe patterns const isValid = SAFE_BASE_IMAGE_PATTERNS.some(pattern => pattern.test(image)); if (isValid) { @@ -134,13 +153,93 @@ export function validateAgentBaseImage(image: string): { valid: boolean; error?: 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' + - ' - ghcr.io/catthehacker/ubuntu:act-XX.XX\n' + - 'Use @sha256:... suffix for digest-pinned versions.' + error: `Invalid agent image: "${image}". ` + + 'For security, only approved images are allowed:\n\n' + + ' Presets (pre-built, fast):\n' + + ' default - Minimal ubuntu:22.04 (~200MB)\n' + + ' act - GitHub Actions parity (~2GB)\n\n' + + ' Custom base images (requires --build-local):\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' + + ' ghcr.io/catthehacker/ubuntu:act-XX.XX\n\n' + + ' Use @sha256:... suffix for digest-pinned versions.' + }; +} + +/** + * Result of processing the agent image option + */ +export interface AgentImageResult { + /** The resolved agent image value */ + agentImage: string; + /** Whether this is a preset (default, act) or custom image */ + isPreset: boolean; + /** Log message to display (info level) */ + infoMessage?: string; + /** Error message if validation failed */ + error?: string; + /** Whether --build-local is required but not provided */ + requiresBuildLocal?: boolean; +} + +/** + * Processes and validates the agent image option. + * This function handles the logic for determining whether the image is valid, + * whether it requires --build-local, and what messages to display. + * + * @param agentImageOption - The --agent-image option value (may be undefined) + * @param buildLocal - Whether --build-local flag was provided + * @returns AgentImageResult with the processed values + */ +export function processAgentImageOption( + agentImageOption: string | undefined, + buildLocal: boolean +): AgentImageResult { + const agentImage = agentImageOption || 'default'; + + // Validate the image (works for both presets and custom images) + const validation = validateAgentImage(agentImage); + if (!validation.valid) { + return { + agentImage, + isPreset: false, + error: validation.error, + }; + } + + const isPreset = isAgentImagePreset(agentImage); + + // Custom images (not presets) require --build-local + if (!isPreset) { + if (!buildLocal) { + return { + agentImage, + isPreset: false, + requiresBuildLocal: true, + error: '❌ Custom agent images require --build-local flag\n Example: awf --build-local --agent-image ghcr.io/catthehacker/ubuntu:runner-22.04 ...', + }; + } + return { + agentImage, + isPreset: false, + infoMessage: `Using custom agent base image: ${agentImage}`, + }; + } + + // Handle presets + if (agentImage === 'act') { + return { + agentImage, + isPreset: true, + infoMessage: 'Using agent image preset: act (GitHub Actions parity)', + }; + } + + // 'default' preset - no special message needed + return { + agentImage, + isPreset: true, }; } @@ -388,12 +487,15 @@ program 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' + '--agent-image ', + 'Agent container image (default: "default")\n' + + ' Presets (pre-built, fast):\n' + + ' default - Minimal ubuntu:22.04 (~200MB)\n' + + ' act - GitHub Actions parity (~2GB)\n' + + ' Custom base images (requires --build-local):\n' + + ' ubuntu:XX.XX\n' + + ' ghcr.io/catthehacker/ubuntu:runner-XX.XX\n' + + ' ghcr.io/catthehacker/ubuntu:full-XX.XX' ) .option( '--image-registry ', @@ -659,6 +761,17 @@ program logger.warn('⚠️ SSL Bump intercepts HTTPS traffic. Only use for trusted workloads.'); } + // Validate agent image option + const agentImageResult = processAgentImageOption(options.agentImage, options.buildLocal); + if (agentImageResult.error) { + logger.error(agentImageResult.error); + process.exit(1); + } + if (agentImageResult.infoMessage) { + logger.info(agentImageResult.infoMessage); + } + const agentImage = agentImageResult.agentImage; + const config: WrapperConfig = { allowedDomains, blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined, @@ -668,7 +781,7 @@ program tty: options.tty || false, workDir: options.workDir, buildLocal: options.buildLocal, - agentBaseImage: options.agentBaseImage, + agentImage, imageRegistry: options.imageRegistry, imageTag: options.imageTag, additionalEnv: Object.keys(additionalEnv).length > 0 ? additionalEnv : undefined, @@ -683,22 +796,6 @@ program allowedUrls, }; - // 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 { - 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 1029e036..04883599 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1,4 +1,4 @@ -import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand, validateIdNotInSystemRange, getSafeHostUid, getSafeHostGid, MIN_REGULAR_UID } from './docker-manager'; +import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand, validateIdNotInSystemRange, getSafeHostUid, getSafeHostGid, MIN_REGULAR_UID, ACT_PRESET_BASE_IMAGE } from './docker-manager'; import { WrapperConfig } from './types'; import * as fs from 'fs'; import * as path from 'path'; @@ -47,6 +47,16 @@ describe('docker-manager', () => { }); }); + describe('ACT_PRESET_BASE_IMAGE', () => { + it('should be a valid catthehacker act image', () => { + expect(ACT_PRESET_BASE_IMAGE).toBe('ghcr.io/catthehacker/ubuntu:act-24.04'); + }); + + it('should match expected pattern for catthehacker images', () => { + expect(ACT_PRESET_BASE_IMAGE).toMatch(/^ghcr\.io\/catthehacker\/ubuntu:act-\d+\.\d+$/); + }); + }); + describe('validateIdNotInSystemRange', () => { it('should return 1000 for system UIDs (0-999)', () => { expect(validateIdNotInSystemRange(0)).toBe('1000'); @@ -205,50 +215,102 @@ describe('docker-manager', () => { expect(result.services.agent.image).toBeUndefined(); }); - it('should pass BASE_IMAGE build arg when agentBaseImage is specified', () => { - const customBaseImageConfig = { + it('should pass BASE_IMAGE build arg when custom agentImage is specified with --build-local', () => { + const customImageConfig = { ...mockConfig, buildLocal: true, - agentBaseImage: 'ghcr.io/catthehacker/ubuntu:runner-22.04', + agentImage: 'ghcr.io/catthehacker/ubuntu:runner-22.04', }; - const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + const result = generateDockerCompose(customImageConfig, 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', () => { + it('should not include BASE_IMAGE build arg when using default agentImage with --build-local', () => { + const localConfig = { ...mockConfig, buildLocal: true, agentImage: 'default' }; + const result = generateDockerCompose(localConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + // BASE_IMAGE should not be set when using the default preset + expect(result.services.agent.build?.args?.BASE_IMAGE).toBeUndefined(); + }); + + it('should not include BASE_IMAGE build arg when agentImage is undefined with --build-local', () => { const localConfig = { ...mockConfig, buildLocal: true }; + // agentImage is not set, should default to 'default' preset 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') + // BASE_IMAGE should not be set when using the default (undefined means 'default') 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 = { + it('should pass BASE_IMAGE build arg when agentImage with SHA256 digest is specified', () => { + const customImageConfig = { ...mockConfig, buildLocal: true, - agentBaseImage: 'ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1', + agentImage: 'ghcr.io/catthehacker/ubuntu:full-22.04@sha256:a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1', }; - const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + const result = generateDockerCompose(customImageConfig, 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 = { + it('should use act base image when agentImage is "act" preset with --build-local', () => { + const actPresetConfig = { ...mockConfig, buildLocal: true, - agentBaseImage: 'ubuntu:22.04', + agentImage: 'act', }; - const result = generateDockerCompose(customBaseImageConfig, mockNetworkConfig); + const result = generateDockerCompose(actPresetConfig, 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'); + // When using 'act' preset with --build-local, should use the catthehacker act image + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe(ACT_PRESET_BASE_IMAGE); + }); + + it('should use agent-act GHCR image when agentImage is "act" preset without --build-local', () => { + const actPresetConfig = { + ...mockConfig, + agentImage: 'act', + }; + const result = generateDockerCompose(actPresetConfig, mockNetworkConfig); + + expect(result.services.agent.image).toBe('ghcr.io/githubnext/gh-aw-firewall/agent-act:latest'); + expect(result.services.agent.build).toBeUndefined(); + }); + + it('should use agent GHCR image when agentImage is "default" preset', () => { + const defaultPresetConfig = { + ...mockConfig, + agentImage: 'default', + }; + const result = generateDockerCompose(defaultPresetConfig, mockNetworkConfig); + + expect(result.services.agent.image).toBe('ghcr.io/githubnext/gh-aw-firewall/agent:latest'); + expect(result.services.agent.build).toBeUndefined(); + }); + + it('should use agent GHCR image when agentImage is undefined', () => { + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + + expect(result.services.agent.image).toBe('ghcr.io/githubnext/gh-aw-firewall/agent:latest'); + expect(result.services.agent.build).toBeUndefined(); + }); + + it('should use custom registry and tag with act preset', () => { + const customConfig = { + ...mockConfig, + agentImage: 'act', + imageRegistry: 'docker.io/myrepo', + imageTag: 'v1.0.0', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services['squid-proxy'].image).toBe('docker.io/myrepo/squid:v1.0.0'); + expect(result.services.agent.image).toBe('docker.io/myrepo/agent-act:v1.0.0'); }); it('should use custom registry and tag', () => { @@ -263,6 +325,69 @@ describe('docker-manager', () => { expect(result.services.agent.image).toBe('docker.io/myrepo/agent:v1.0.0'); }); + it('should use custom registry and tag with default preset explicitly set', () => { + const customConfig = { + ...mockConfig, + agentImage: 'default', + imageRegistry: 'docker.io/myrepo', + imageTag: 'v2.0.0', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services.agent.image).toBe('docker.io/myrepo/agent:v2.0.0'); + expect(result.services.agent.build).toBeUndefined(); + }); + + it('should build locally with custom catthehacker full image', () => { + const customConfig = { + ...mockConfig, + buildLocal: true, + agentImage: 'ghcr.io/catthehacker/ubuntu:full-24.04', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe('ghcr.io/catthehacker/ubuntu:full-24.04'); + expect(result.services.agent.image).toBeUndefined(); + }); + + it('should build locally with custom ubuntu image', () => { + const customConfig = { + ...mockConfig, + buildLocal: true, + agentImage: 'ubuntu:24.04', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services.agent.build).toBeDefined(); + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe('ubuntu:24.04'); + }); + + it('should include USER_UID and USER_GID in build args with custom image', () => { + const customConfig = { + ...mockConfig, + buildLocal: true, + agentImage: 'ghcr.io/catthehacker/ubuntu:runner-22.04', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services.agent.build?.args?.USER_UID).toBeDefined(); + expect(result.services.agent.build?.args?.USER_GID).toBeDefined(); + }); + + it('should include USER_UID and USER_GID in build args with act preset', () => { + const customConfig = { + ...mockConfig, + buildLocal: true, + agentImage: 'act', + }; + const result = generateDockerCompose(customConfig, mockNetworkConfig); + + expect(result.services.agent.build?.args?.USER_UID).toBeDefined(); + expect(result.services.agent.build?.args?.USER_GID).toBeDefined(); + expect(result.services.agent.build?.args?.BASE_IMAGE).toBe(ACT_PRESET_BASE_IMAGE); + }); + it('should configure network with correct IPs', () => { const result = generateDockerCompose(mockConfig, mockNetworkConfig); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 5bbb7b05..b9a50fbb 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -11,6 +11,12 @@ import { generateSessionCa, initSslDb, CaFiles, parseUrlPatterns } from './ssl-b const SQUID_PORT = 3128; const SQUID_INTERCEPT_PORT = 3129; // Port for transparently intercepted traffic +/** + * Base image for the 'act' preset when building locally. + * Uses catthehacker's GitHub Actions parity image. + */ +export const ACT_PRESET_BASE_IMAGE = 'ghcr.io/catthehacker/ubuntu:act-24.04'; + /** * Minimum UID/GID value for regular users. * UIDs 0-999 are reserved for system users on most Linux distributions. @@ -419,8 +425,15 @@ export function generateDockerCompose( } // Use GHCR image or build locally - if (useGHCR) { - agentService.image = `${registry}/agent:${tag}`; + // For presets ('default', 'act'), use GHCR images + // For custom images, build locally with the custom base image + const agentImage = config.agentImage || 'default'; + const isPreset = agentImage === 'default' || agentImage === 'act'; + + if (useGHCR && isPreset) { + // Use pre-built GHCR image based on preset + const imageName = agentImage === 'act' ? 'agent-act' : 'agent'; + agentService.image = `${registry}/${imageName}:${tag}`; } else { const buildArgs: Record = { // Pass host UID/GID to match file ownership in container @@ -429,10 +442,15 @@ export function generateDockerCompose( USER_GID: getSafeHostGid(), }; - // Allow custom base image for closer parity with GitHub Actions runner - if (config.agentBaseImage) { - buildArgs.BASE_IMAGE = config.agentBaseImage; + // For custom images (not presets), pass as BASE_IMAGE build arg + // For 'act' preset with --build-local, use the act base image + if (!isPreset) { + buildArgs.BASE_IMAGE = agentImage; + } else if (agentImage === 'act') { + // When building locally with 'act' preset, use the catthehacker act image + buildArgs.BASE_IMAGE = ACT_PRESET_BASE_IMAGE; } + // For 'default' preset with --build-local, use the Dockerfile's default (ubuntu:22.04) agentService.build = { context: path.join(projectRoot, 'containers/agent'), diff --git a/src/types.ts b/src/types.ts index 966b195f..caca3f85 100644 --- a/src/types.ts +++ b/src/types.ts @@ -148,20 +148,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' + * Agent container image preset or custom base image + * + * Presets (pre-built, fast startup): + * - 'default' or undefined: Minimal ubuntu:22.04 (~200MB) - uses GHCR agent:tag + * - 'act': GitHub Actions parity (~2GB) - uses GHCR agent-act:tag + * + * Custom base images (require --build-local): + * - 'ubuntu:XX.XX': Official Ubuntu image + * - 'ghcr.io/catthehacker/ubuntu:runner-XX.XX': Closer to GitHub Actions runner (~2-5GB) + * - 'ghcr.io/catthehacker/ubuntu:full-XX.XX': Near-identical to GitHub Actions runner (~20GB) + * + * @default 'default' + * @example 'act' * @example 'ghcr.io/catthehacker/ubuntu:runner-22.04' */ - agentBaseImage?: string; + agentImage?: 'default' | 'act' | string; /** * Additional environment variables to pass to the agent execution container