-
Notifications
You must be signed in to change notification settings - Fork 28
feat: phase 1 – gh CLI proxy sidecar with mcpg DIFC proxy #1730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2d95229
Initial plan
Copilot 20ed5b7
feat: implement Phase 1 - CLI proxy sidecar with mcpg DIFC proxy
Copilot e02f2fb
fix: address code review and CodeQL feedback
Copilot 1b3964e
fix: address all review feedback on cli-proxy
Copilot d99431d
fix: api read-only deny, trusted-bots, pin mcpg image, comments
Copilot 3f51a22
feat: add --cli-proxy-mcpg-image for compiler-controlled mcpg version
Copilot 9fe138f
feat: bump default mcpg image to v0.2.15
Copilot d1f10eb
test: add coverage for enableCliProxy in predownload and CLI
lpcox 81de86c
test: add coverage for emitCliProxyStatusLogs in cli.ts
lpcox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #!/bin/sh | ||
| # /usr/local/bin/gh-cli-proxy-wrapper | ||
| # Forwards gh CLI invocations to the CLI proxy sidecar over HTTP. | ||
| # This wrapper is installed at /usr/local/bin/gh in the agent container | ||
| # when --enable-cli-proxy is active, so it takes precedence over any | ||
| # host-mounted gh binary at /host/usr/bin/gh. | ||
| # | ||
| # Dependencies: curl, jq (both available in the agent container) | ||
|
|
||
| CLI_PROXY="${AWF_CLI_PROXY_URL:-http://172.30.0.50:11000}" | ||
|
|
||
| # Build JSON array from all positional arguments | ||
| ARGS_JSON='[]' | ||
| if [ $# -gt 0 ]; then | ||
| ARGS_JSON=$(printf '%s\n' "$@" | jq -R . | jq -s .) | ||
| fi | ||
|
|
||
| # Capture working directory | ||
| CWD=$(pwd) | ||
|
|
||
| # Read stdin if data is available (non-interactive) | ||
| STDIN_DATA="" | ||
| if [ ! -t 0 ]; then | ||
| STDIN_DATA=$(cat | base64 | tr -d '\n') | ||
| fi | ||
|
|
||
| # Use a temporary file to capture the response body without -f, | ||
| # so we can read the body even on 4xx/5xx responses (e.g., 403 policy block). | ||
| RESPONSE_FILE=$(mktemp) | ||
| HTTP_STATUS=$(curl -s \ | ||
| --max-time 60 \ | ||
| -o "$RESPONSE_FILE" \ | ||
| -w "%{http_code}" \ | ||
| -X POST "${CLI_PROXY}/exec" \ | ||
| -H "Content-Type: application/json" \ | ||
| --data-binary "$(printf '{"args":%s,"cwd":%s,"stdin":"%s"}' \ | ||
| "$ARGS_JSON" \ | ||
| "$(printf '%s' "$CWD" | jq -Rs .)" \ | ||
| "$STDIN_DATA")") | ||
| CURL_EXIT=$? | ||
| RESPONSE=$(cat "$RESPONSE_FILE") | ||
| rm -f "$RESPONSE_FILE" | ||
|
|
||
| if [ "$CURL_EXIT" -ne 0 ]; then | ||
| echo "gh: CLI proxy unavailable at ${CLI_PROXY} (curl exit ${CURL_EXIT})" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Surface policy errors (403), request errors (400/413), and server errors (5xx) | ||
| if [ "$HTTP_STATUS" != "200" ]; then | ||
| ERROR=$(printf '%s' "$RESPONSE" | jq -r '.error // empty' 2>/dev/null) | ||
| if [ -n "$ERROR" ]; then | ||
| echo "gh: ${ERROR}" >&2 | ||
| else | ||
| echo "gh: CLI proxy returned HTTP ${HTTP_STATUS}" >&2 | ||
| fi | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Extract and emit stdout/stderr from a successful 200 response | ||
| STDOUT=$(printf '%s' "$RESPONSE" | jq -r '.stdout // empty' 2>/dev/null) | ||
| STDERR=$(printf '%s' "$RESPONSE" | jq -r '.stderr // empty' 2>/dev/null) | ||
| EXIT_CODE=$(printf '%s' "$RESPONSE" | jq -r '.exitCode // 1' 2>/dev/null) | ||
|
|
||
| printf '%s' "$STDOUT" | ||
| printf '%s' "$STDERR" >&2 | ||
| exit "${EXIT_CODE:-1}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # CLI Proxy sidecar for AWF - provides gh CLI access with 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 | ||
| FROM node:22-alpine | ||
|
|
||
| # Install gh CLI and curl for healthchecks/wrapper | ||
| # gh CLI is available in the Alpine community repository | ||
| RUN apk add --no-cache \ | ||
| curl \ | ||
| github-cli \ | ||
| 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 | ||
|
|
||
| # Copy package files | ||
| COPY package*.json ./ | ||
|
|
||
| # Install dependencies from lockfile (deterministic) | ||
| RUN npm ci --omit=dev | ||
|
|
||
| # Copy application files | ||
| COPY server.js ./ | ||
| COPY entrypoint.sh /usr/local/bin/entrypoint.sh | ||
| COPY healthcheck.sh /usr/local/bin/healthcheck.sh | ||
|
|
||
| RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh | ||
|
|
||
| # Create non-root user | ||
| 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 && \ | ||
| chown -R cliproxy:cliproxy /var/log/cli-proxy | ||
|
|
||
| # Create /tmp/proxy-tls directory owned by cliproxy for mcpg TLS cert generation | ||
| RUN mkdir -p /tmp/proxy-tls && chown cliproxy:cliproxy /tmp/proxy-tls | ||
|
|
||
| # Switch to non-root user | ||
| USER cliproxy | ||
|
|
||
| # Expose port for agent→cli-proxy HTTP communication | ||
| # 11000 - gh exec endpoint and health check | ||
| EXPOSE 11000 | ||
|
|
||
| ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| #!/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. | ||
| 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..." | ||
|
|
||
| mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg | ||
|
|
||
| # 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) | ||
| echo "[cli-proxy] Waiting for mcpg TLS certificate..." | ||
| i=0 | ||
| while [ $i -lt 30 ]; do | ||
| if [ -f /tmp/proxy-tls/ca.crt ]; then | ||
| echo "[cli-proxy] TLS certificate available" | ||
| break | ||
| fi | ||
| sleep 1 | ||
| i=$((i + 1)) | ||
| 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 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA) | ||
| export GH_HOST="localhost:18443" | ||
| export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt" | ||
| export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}" | ||
|
|
||
| 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() { | ||
| 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 | ||
|
|
||
| # Start the Node.js HTTP server in the background so the shell keeps running | ||
| # and traps remain active for graceful shutdown. | ||
| echo "[cli-proxy] Starting HTTP server on port 11000..." | ||
| node /app/server.js & | ||
| NODE_PID=$! | ||
|
|
||
| # Wait for Node to exit and propagate its exit code | ||
| if wait "$NODE_PID"; then | ||
| NODE_EXIT=0 | ||
| else | ||
| NODE_EXIT=$? | ||
| fi | ||
|
|
||
| cleanup | ||
| exit "$NODE_EXIT" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/bin/sh | ||
| # Healthcheck for the CLI proxy sidecar | ||
| # Verifies the HTTP server is responsive | ||
| curl -sf --max-time 3 http://localhost:11000/health > /dev/null |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The image creates a non-root user (cliproxy) but never switches to it, so both mcpg and the HTTP server will run as root. This also contradicts the comment in src/docker-manager.ts that the CLI proxy runs as non-root. Add a
USER cliproxy(or equivalent) before the entrypoint and ensure required paths (/var/log/cli-proxy, /tmp/proxy-tls) remain writable.