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
6 changes: 5 additions & 1 deletion containers/agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ RUN set -eux; \

# Upgrade all packages to pick up security patches
# Addresses CVE-2023-44487 (HTTP/2 Rapid Reset) and other known vulnerabilities
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
# Retry logic handles transient mirror sync failures during apt-get update
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* || \
(echo "apt-get upgrade failed, retrying with fresh package index..." && \
rm -rf /var/lib/apt/lists/* && \
apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*)

# Create non-root user with UID/GID matching host user
# This allows the user command to run with appropriate permissions
Expand Down
31 changes: 8 additions & 23 deletions containers/cli-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
# CLI Proxy sidecar for AWF - provides gh CLI access with mcpg DIFC proxy
# CLI Proxy sidecar for AWF - provides gh CLI access via mcpg DIFC proxy
#
# Multi-stage build:
# Stage 1: Extract mcpg binary from ghcr.io/github/gh-aw-mcpg image
# Stage 2: Assemble final image with gh CLI, Node.js, and mcpg
#
# This container runs two processes:
# 1. mcpg proxy (TLS, port 18443) - holds GH_TOKEN, enforces guard policies
# 2. HTTP server (port 11000) - receives gh invocations from the agent container

# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image.
# MCPG_IMAGE is configurable via --cli-proxy-mcpg-image so the AWF compiler
# can control which mcpg version is pulled and run (e.g. for version pinning
# or testing a new mcpg release before it is bundled in the GHCR cli-proxy image).
ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.15
FROM ${MCPG_IMAGE} AS mcpg-source

# Stage 2: Build the CLI proxy image
# This container runs the HTTP exec server (port 11000) that receives gh
# invocations from the agent container. The mcpg DIFC proxy runs as a
# separate docker-compose service (awf-cli-proxy-mcpg) using the official
# gh-aw-mcpg image directly — no binary extraction needed. GH_HOST is
# set to the mcpg container so all gh CLI traffic flows through the proxy.
FROM node:22-alpine

# Install gh CLI and curl for healthchecks/wrapper
Expand All @@ -26,10 +15,6 @@ RUN apk add --no-cache \
ca-certificates \
bash

# Copy the mcpg binary from the mcpg-source stage
COPY --from=mcpg-source /usr/local/bin/mcpg /usr/local/bin/mcpg
RUN chmod +x /usr/local/bin/mcpg

# Create app directory
WORKDIR /app

Expand All @@ -50,10 +35,10 @@ RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh
RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy

# Create log directory owned by cliproxy (so non-root process can write)
RUN mkdir -p /var/log/cli-proxy/mcpg && \
RUN mkdir -p /var/log/cli-proxy && \
chown -R cliproxy:cliproxy /var/log/cli-proxy

# Create /tmp/proxy-tls directory owned by cliproxy for mcpg TLS cert generation
# Create /tmp/proxy-tls directory owned by cliproxy for shared mcpg TLS certs
RUN mkdir -p /tmp/proxy-tls && chown cliproxy:cliproxy /tmp/proxy-tls

# Switch to non-root user
Expand Down
64 changes: 16 additions & 48 deletions containers/cli-proxy/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,51 +1,24 @@
#!/bin/bash
# CLI Proxy sidecar entrypoint
# Starts the mcpg DIFC proxy (GH_TOKEN required), then starts the Node.js HTTP server
# under a supervisor loop so signals are properly handled and mcpg is cleaned up.
#
# The mcpg DIFC proxy runs as a separate docker-compose service
# (awf-cli-proxy-mcpg). This container shares mcpg's network namespace
# (network_mode: service:cli-proxy-mcpg), so localhost resolves to mcpg.
# This ensures the TLS cert's SAN (localhost + 127.0.0.1) matches the
# hostname used by the gh CLI, avoiding TLS hostname verification failures.
set -e

echo "[cli-proxy] Starting CLI proxy sidecar..."

MCPG_PID=""
NODE_PID=""

# GH_TOKEN is required: without it, mcpg cannot authenticate and DIFC guard policies
# cannot be enforced. Fail closed rather than starting an unenforced server.
if [ -z "$GH_TOKEN" ]; then
echo "[cli-proxy] ERROR: GH_TOKEN not set - refusing to start without mcpg DIFC enforcement"
exit 1
fi

echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..."
# cli-proxy shares mcpg's network namespace, so mcpg is always at localhost.
# AWF_MCPG_PORT is set by docker-manager.ts.
MCPG_PORT="${AWF_MCPG_PORT:-18443}"

mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg
echo "[cli-proxy] mcpg proxy at localhost:${MCPG_PORT}"

# Build the guard policy JSON if not explicitly provided
if [ -z "$AWF_GH_GUARD_POLICY" ]; then
if [ -n "$GITHUB_REPOSITORY" ]; then
AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}"
else
AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}"
fi
echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}"
else
echo "[cli-proxy] Using provided guard policy"
fi

# Start mcpg proxy in background
# mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding
mcpg proxy \
--policy "${AWF_GH_GUARD_POLICY}" \
--listen 127.0.0.1:18443 \
--tls \
--tls-dir /tmp/proxy-tls \
--guards-mode filter \
--trusted-bots "github-actions[bot],github-actions,dependabot[bot],copilot" \
--log-dir /var/log/cli-proxy/mcpg &
MCPG_PID=$!
echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})"

# Wait for TLS cert to be generated (max 30s)
# Wait for TLS cert to appear in the shared volume (max 30s)
echo "[cli-proxy] Waiting for mcpg TLS certificate..."
i=0
while [ $i -lt 30 ]; do
Expand All @@ -58,31 +31,26 @@ while [ $i -lt 30 ]; do
done

if [ ! -f /tmp/proxy-tls/ca.crt ]; then
echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s"
kill "$MCPG_PID" 2>/dev/null || true
echo "[cli-proxy] ERROR: mcpg TLS certificate not found within 30s"
exit 1
fi

# Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA)
export GH_HOST="localhost:18443"
# Uses localhost because cli-proxy shares mcpg's network namespace — the
# self-signed cert's SAN covers localhost, so TLS hostname verification passes.
export GH_HOST="localhost:${MCPG_PORT}"
export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt"
export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}"
Comment on lines 37 to 43
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The mcpg container healthcheck validates https://localhost:${MCPG_PORT} with the generated cert, but the CLI proxy configures GH_HOST to ${MCPG_HOST}:${MCPG_PORT} (typically an IP like 172.30.0.51). If the mcpg server cert is issued only for localhost (common for self-signed startup certs), gh will fail TLS hostname verification when connecting via IP. Consider aligning the hostname used by clients and healthchecks (e.g., use the service DNS name) and/or configuring mcpg to generate a cert whose SAN covers the chosen hostname.

This issue also appears on line 38 of the same file.

Copilot uses AI. Check for mistakes.

echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}"

# Cleanup handler: stop both the Node HTTP server and mcpg when we receive a signal
# or when the server exits. This runs correctly because we do NOT exec Node — we
# start it in the background and wait, so the shell (and its traps) remain active.
# Cleanup handler: stop the Node HTTP server on signal
cleanup() {
echo "[cli-proxy] Shutting down..."
if [ -n "$NODE_PID" ]; then
kill "$NODE_PID" 2>/dev/null || true
wait "$NODE_PID" 2>/dev/null || true
fi
if [ -n "$MCPG_PID" ]; then
kill "$MCPG_PID" 2>/dev/null || true
wait "$MCPG_PID" 2>/dev/null || true
fi
}
trap 'cleanup; exit 0' INT TERM

Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ echo "==========================================="

# First, explicitly remove containers by name (handles orphaned containers)
echo "Removing awf containers by name..."
docker rm -f awf-squid awf-agent awf-iptables-init awf-api-proxy awf-cli-proxy 2>/dev/null || true
docker rm -f awf-squid awf-agent awf-iptables-init awf-api-proxy awf-cli-proxy awf-cli-proxy-mcpg 2>/dev/null || true

# Cleanup diagnostic test containers
echo "Stopping docker compose services..."
Expand Down
8 changes: 5 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1454,9 +1454,8 @@ program
)
.option(
'--cli-proxy-mcpg-image <image>',
'Docker image for the mcpg DIFC proxy used inside the CLI proxy sidecar\n' +
' (only used with --build-local; ignored when pulling pre-built GHCR images)\n' +
' Set by the AWF compiler to control which mcpg version is pulled and run',
'Docker image for the mcpg DIFC proxy container (runs as a separate service alongside cli-proxy)\n' +
' Set by the AWF compiler to control which mcpg version is used',
'ghcr.io/github/gh-aw-mcpg:v0.2.15'
)
// -- Logging & Debug --
Expand Down Expand Up @@ -2058,6 +2057,7 @@ export async function handlePredownloadAction(options: {
agentImage: string;
enableApiProxy: boolean;
enableCliProxy?: boolean;
cliProxyMcpgImage?: string;
}): Promise<void> {
const { predownloadCommand } = await import('./commands/predownload');
try {
Expand All @@ -2067,6 +2067,7 @@ export async function handlePredownloadAction(options: {
agentImage: options.agentImage,
enableApiProxy: options.enableApiProxy,
enableCliProxy: options.enableCliProxy,
cliProxyMcpgImage: options.cliProxyMcpgImage,
});
} catch (error) {
const exitCode = (error as Error & { exitCode?: number }).exitCode ?? 1;
Expand All @@ -2091,6 +2092,7 @@ program
)
.option('--enable-api-proxy', 'Also download the API proxy image', false)
.option('--enable-cli-proxy', 'Also download the CLI proxy image', false)
.option('--cli-proxy-mcpg-image <image>', 'Docker image for the mcpg DIFC proxy container', 'ghcr.io/github/gh-aw-mcpg:v0.2.15')
.action(handlePredownloadAction);

// Logs subcommand - view Squid proxy logs
Expand Down
23 changes: 20 additions & 3 deletions src/commands/predownload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ describe('predownload', () => {
]);
});

it('should include cli-proxy when enabled', () => {
it('should include cli-proxy and mcpg when enabled', () => {
const images = resolveImages({ ...defaults, enableCliProxy: true });
expect(images).toEqual([
'ghcr.io/github/gh-aw-firewall/squid:latest',
'ghcr.io/github/gh-aw-firewall/agent:latest',
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
'ghcr.io/github/gh-aw-mcpg:v0.2.15',
]);
});

Expand All @@ -59,6 +60,17 @@ describe('predownload', () => {
'ghcr.io/github/gh-aw-firewall/agent:latest',
'ghcr.io/github/gh-aw-firewall/api-proxy:latest',
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
'ghcr.io/github/gh-aw-mcpg:v0.2.15',
]);
});

it('should use custom mcpg image when specified', () => {
const images = resolveImages({ ...defaults, enableCliProxy: true, cliProxyMcpgImage: 'ghcr.io/github/gh-aw-mcpg:v0.3.0' });
expect(images).toEqual([
'ghcr.io/github/gh-aw-firewall/squid:latest',
'ghcr.io/github/gh-aw-firewall/agent:latest',
'ghcr.io/github/gh-aw-firewall/cli-proxy:latest',
'ghcr.io/github/gh-aw-mcpg:v0.3.0',
]);
});

Expand Down Expand Up @@ -135,15 +147,20 @@ describe('predownload', () => {
);
});

it('should pull cli-proxy when enabled', async () => {
it('should pull cli-proxy and mcpg when enabled', async () => {
await predownloadCommand({ ...defaults, enableCliProxy: true });

expect(execa).toHaveBeenCalledTimes(3);
expect(execa).toHaveBeenCalledTimes(4);
expect(execa).toHaveBeenCalledWith(
'docker',
['pull', 'ghcr.io/github/gh-aw-firewall/cli-proxy:latest'],
{ stdio: 'inherit' },
);
expect(execa).toHaveBeenCalledWith(
'docker',
['pull', 'ghcr.io/github/gh-aw-mcpg:v0.2.15'],
{ stdio: 'inherit' },
);
});

it('should throw with exitCode 1 when a pull fails', async () => {
Expand Down
7 changes: 6 additions & 1 deletion src/commands/predownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface PredownloadOptions {
agentImage: string;
enableApiProxy: boolean;
enableCliProxy?: boolean;
cliProxyMcpgImage?: string;
}

/**
Expand Down Expand Up @@ -48,9 +49,13 @@ export function resolveImages(options: PredownloadOptions): string[] {
images.push(`${imageRegistry}/api-proxy:${imageTag}`);
}

// Optionally pull cli-proxy
// Optionally pull cli-proxy and its mcpg sidecar
if (options.enableCliProxy) {
images.push(`${imageRegistry}/cli-proxy:${imageTag}`);
// mcpg runs as a separate container; default or user-specified image
const mcpgImage = options.cliProxyMcpgImage || 'ghcr.io/github/gh-aw-mcpg:v0.2.15';
validateImageReference(mcpgImage);
images.push(mcpgImage);
}

return images;
Expand Down
Loading
Loading