feat: phase 1 – gh CLI proxy sidecar with mcpg DIFC proxy#1730
Conversation
- Add containers/cli-proxy/ with Dockerfile, server.js, entrypoint.sh, healthcheck.sh, package.json, and server.test.js (49 unit tests) - Add CLI_PROXY_PORT constant and enableCliProxy/cliProxyWritable/ cliProxyPolicy/githubToken config fields to src/types.ts - Add cli-proxy service definition in src/docker-manager.ts: - IP 172.30.0.50 on awf-net - Passes GH_TOKEN to mcpg DIFC proxy; never exposed to agent - AWF_CLI_PROXY_URL set in agent environment - GITHUB_TOKEN/GH_TOKEN excluded from agent when cli-proxy enabled - AWF_CLI_PROXY_IP propagated to iptables-init - Log preservation in cleanup - Add --enable-cli-proxy, --cli-proxy-writable, --cli-proxy-policy flags to src/cli.ts; add --enable-cli-proxy to predownload subcommand - Add AWF_CLI_PROXY_IP iptables RETURN rule in setup-iptables.sh - Add gh-cli-proxy-wrapper.sh to agent container; activated at runtime via AWF_CLI_PROXY_URL in both chroot and non-chroot modes - Add 18 new docker-manager tests for CLI proxy service; update existing container removal test to include awf-cli-proxy Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b5f7487a-4993-4737-a01e-8f091a8e84c7
- Fix test name: 'should not include cli-proxy when cliProxyIp is missing' - Fix gh-cli-proxy-wrapper.sh: capture curl exit code in CURL_EXIT variable before checking it (avoids $? being overwritten by shell); remove set -e - Fix entrypoint.sh: clarify log message that /tmp/awf-lib/gh is the path inside the chroot (not the host-prefixed /host/tmp/awf-lib/gh path) - Fix server.js: sanitize error message in catch block to avoid stack trace exposure (CodeQL js/stack-trace-exposure) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b5f7487a-4993-4737-a01e-8f091a8e84c7
There was a problem hiding this comment.
Pull request overview
Adds an AWF “gh CLI proxy” sidecar (mcpg DIFC proxy + HTTP exec server) so agents can invoke gh without receiving GitHub tokens, while maintaining guard/audit controls.
Changes:
- Introduces
containers/cli-proxy/image (mcpg + Node HTTP server) and an agent-sideghwrapper that forwards commands over HTTP. - Extends AWF config/CLI flags and docker-compose generation to deploy and wire the cli-proxy sidecar (IP
172.30.0.50, port11000) and update iptables NAT exceptions. - Adds/updates unit tests around docker-compose generation and image predownload resolution.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Adds CLI proxy port constant and WrapperConfig fields for enabling/configuring the sidecar. |
| src/docker-manager.ts | Adds compose service generation, env propagation, token exclusion from agent env, and log preservation for cli-proxy. |
| src/docker-manager.test.ts | Adds coverage for cli-proxy service generation and container cleanup list updates. |
| src/commands/predownload.ts | Adds optional predownload of the cli-proxy image. |
| src/cli.ts | Adds CLI flags and config wiring for enabling/configuring the cli-proxy sidecar. |
| containers/cli-proxy/Dockerfile | Builds the new sidecar image (mcpg + gh + Node server). |
| containers/cli-proxy/entrypoint.sh | Starts mcpg (if token present) and the HTTP server. |
| containers/cli-proxy/server.js | Implements /health and /exec with subcommand/action policy enforcement. |
| containers/cli-proxy/server.test.js | Adds unit tests for argument validation/policy logic. |
| containers/cli-proxy/package.json | Defines cli-proxy Node package scripts/deps for tests. |
| containers/cli-proxy/healthcheck.sh | Adds a container healthcheck script. |
| containers/agent/setup-iptables.sh | Adds NAT RETURN rule to allow agent→cli-proxy traffic. |
| containers/agent/gh-cli-proxy-wrapper.sh | Adds the agent-side gh wrapper that forwards invocations to the sidecar. |
| containers/agent/entrypoint.sh | Activates the gh wrapper in both chroot and non-chroot modes when enabled. |
| containers/agent/Dockerfile | Installs the gh wrapper into the agent image. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 15/16 changed files
- Comments generated: 9
| # Create non-root user | ||
| RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy | ||
|
|
||
| # Expose port for agent→cli-proxy HTTP communication | ||
| # 11000 - gh exec endpoint and health check | ||
| EXPOSE 11000 | ||
|
|
||
| ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] |
There was a problem hiding this comment.
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.
| const ALLOWED_SUBCOMMANDS_READONLY = new Set([ | ||
| 'api', | ||
| 'browse', | ||
| 'cache', | ||
| 'codespace', | ||
| 'gist', | ||
| 'issue', | ||
| 'label', | ||
| 'org', | ||
| 'pr', | ||
| 'release', | ||
| 'repo', | ||
| 'run', | ||
| 'search', | ||
| 'secret', | ||
| 'variable', | ||
| 'workflow', | ||
| ]); | ||
|
|
||
| /** | ||
| * Actions that are blocked within their parent subcommand in read-only mode. | ||
| * Maps subcommand -> Set of blocked action verbs. | ||
| */ | ||
| const BLOCKED_ACTIONS_READONLY = new Map([ | ||
| ['gist', new Set(['create', 'delete', 'edit'])], | ||
| ['issue', new Set(['create', 'close', 'delete', 'edit', 'lock', 'pin', 'reopen', 'transfer', 'unpin'])], | ||
| ['label', new Set(['create', 'delete', 'edit'])], | ||
| ['pr', new Set(['checkout', 'close', 'create', 'edit', 'lock', 'merge', 'ready', 'reopen', 'review', 'update-branch'])], | ||
| ['release', new Set(['create', 'delete', 'delete-asset', 'edit', 'upload'])], | ||
| ['repo', new Set(['archive', 'create', 'delete', 'edit', 'fork', 'rename', 'set-default', 'sync', 'unarchive'])], | ||
| ['run', new Set(['cancel', 'delete', 'download', 'rerun'])], | ||
| ['secret', new Set(['delete', 'set'])], | ||
| ['variable', new Set(['delete', 'set'])], | ||
| ['workflow', new Set(['disable', 'enable', 'run'])], | ||
| ]); |
There was a problem hiding this comment.
In read-only mode, some allowed top-level subcommands (e.g. codespace, cache, org) can still perform write operations, but there are no action-level blocks for them in BLOCKED_ACTIONS_READONLY. That means commands like gh codespace create or gh cache delete would currently be permitted in read-only mode. Either remove these subcommands from the read-only allowlist or add an explicit blocked-actions set for each to prevent state-changing actions.
| // Check action-level blocklist | ||
| const blockedActions = BLOCKED_ACTIONS_READONLY.get(subcommand); | ||
| if (blockedActions) { | ||
| // The action is the first non-flag argument after the subcommand | ||
| const subcommandIndex = args.indexOf(subcommand); | ||
| const action = args.slice(subcommandIndex + 1).find(a => !a.startsWith('-')); | ||
| if (action && blockedActions.has(action)) { | ||
| return { | ||
| valid: false, | ||
| error: `Action '${subcommand} ${action}' is not allowed in read-only mode. Enable write mode with --cli-proxy-writable.`, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Action detection uses args.indexOf(subcommand) to find the subcommand position. If the subcommand string appears earlier as a flag value (e.g. gh --repo pr pr merge 1), indexOf will point at the flag value and the blocked-action check can be bypassed. Track and reuse the actual subcommand index found during the scan instead of calling indexOf.
containers/cli-proxy/server.js
Outdated
| function readBody(req) { | ||
| return new Promise((resolve, reject) => { | ||
| const chunks = []; | ||
| req.on('data', chunk => chunks.push(chunk)); | ||
| req.on('end', () => resolve(Buffer.concat(chunks))); | ||
| req.on('error', reject); | ||
| }); |
There was a problem hiding this comment.
readBody() buffers the entire request body without any size limit. A large POST to /exec could cause memory exhaustion in the sidecar. Enforce a maximum request size (e.g., reject once a byte limit is exceeded) and return 413 for oversized bodies.
| RESPONSE=$(curl -sf \ | ||
| --max-time 60 \ | ||
| -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=$? | ||
|
|
||
| if [ "$CURL_EXIT" -ne 0 ] || [ -z "$RESPONSE" ]; then | ||
| echo "gh: CLI proxy unavailable at ${CLI_PROXY}" >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
The wrapper uses curl -f, so any non-2xx response (e.g. 403 when a command is blocked) makes curl exit non-zero and discards the JSON error body. As a result, users see “CLI proxy unavailable” instead of the actual policy error. Capture HTTP status and response body (without -f) and surface .error for 4xx/5xx responses.
src/cli.ts
Outdated
| enableCliProxy: options.enableCliProxy, | ||
| cliProxyWritable: options.cliProxyWritable, | ||
| cliProxyPolicy: options.cliProxyPolicy, | ||
| githubToken: process.env.GITHUB_TOKEN, |
There was a problem hiding this comment.
githubToken is read only from process.env.GITHUB_TOKEN. Many users of gh set GH_TOKEN instead, and this PR also explicitly excludes GH_TOKEN from the agent env when the sidecar is enabled. Consider reading process.env.GITHUB_TOKEN || process.env.GH_TOKEN (and/or honoring a dedicated CLI flag) so enabling the CLI proxy works with either token variable.
| githubToken: process.env.GITHUB_TOKEN, | |
| githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN, |
containers/cli-proxy/entrypoint.sh
Outdated
| # Cleanup handler: kill mcpg when the server exits or receives a signal | ||
| cleanup() { | ||
| echo "[cli-proxy] Shutting down..." | ||
| if [ -n "$MCPG_PID" ]; then | ||
| kill "$MCPG_PID" 2>/dev/null || true | ||
| fi | ||
| exit 0 | ||
| } | ||
| trap cleanup INT TERM | ||
|
|
||
| # Start the Node.js HTTP server (foreground) | ||
| echo "[cli-proxy] Starting HTTP server on port 11000..." | ||
| exec node /app/server.js |
There was a problem hiding this comment.
The entrypoint sets a trap to kill the mcpg background process, but then execs Node as PID 1. After exec, the shell (and its traps) no longer exist, so the cleanup handler won’t run on SIGTERM/SIGINT. If graceful shutdown of mcpg is desired, run a simple supervisor loop (or avoid exec and wait on Node) so signals can trigger cleanup.
| # Cleanup handler: kill mcpg when the server exits or receives a signal | |
| cleanup() { | |
| echo "[cli-proxy] Shutting down..." | |
| if [ -n "$MCPG_PID" ]; then | |
| kill "$MCPG_PID" 2>/dev/null || true | |
| fi | |
| exit 0 | |
| } | |
| trap cleanup INT TERM | |
| # Start the Node.js HTTP server (foreground) | |
| echo "[cli-proxy] Starting HTTP server on port 11000..." | |
| exec node /app/server.js | |
| NODE_PID="" | |
| # Cleanup handler: stop child processes when the server exits or receives a 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 | |
| # Start the Node.js HTTP server under shell supervision so traps remain active | |
| echo "[cli-proxy] Starting HTTP server on port 11000..." | |
| node /app/server.js & | |
| NODE_PID=$! | |
| if wait "$NODE_PID"; then | |
| NODE_EXIT=0 | |
| else | |
| NODE_EXIT=$? | |
| fi | |
| cleanup | |
| exit "$NODE_EXIT" |
containers/cli-proxy/entrypoint.sh
Outdated
|
|
||
| echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}" | ||
| else | ||
| echo "[cli-proxy] WARNING: GH_TOKEN not set - mcpg proxy disabled, gh CLI will not authenticate" |
There was a problem hiding this comment.
When GH_TOKEN is missing, the entrypoint disables mcpg and starts the HTTP server anyway. That means gh commands run without DIFC guard enforcement/audit logging (and may still access public GitHub endpoints), which undermines the “same DIFC guarantees” goal. Consider failing closed (exit non-zero) or having the server reject /exec until mcpg is running, when --enable-cli-proxy is enabled but no token is provided.
| echo "[cli-proxy] WARNING: GH_TOKEN not set - mcpg proxy disabled, gh CLI will not authenticate" | |
| echo "[cli-proxy] ERROR: GH_TOKEN not set - refusing to start HTTP server without mcpg DIFC enforcement" | |
| exit 1 |
| "scripts": { | ||
| "start": "node server.js", | ||
| "test": "jest --verbose --ci" | ||
| }, | ||
| "devDependencies": { | ||
| "jest": "^30.2.0" | ||
| }, |
There was a problem hiding this comment.
This package defines unit tests (npm test), but the repository’s CI workflow currently only runs container tests for containers/api-proxy (see .github/workflows/build.yml:62-66). As a result, changes to cli-proxy (server.js/validateArgs policy) won’t be exercised in CI. Add a CI step to run cd containers/cli-proxy && npm ci && npm test (or otherwise wire these tests into the main test pipeline).
|
@copilot apply changes based on the comments in this thread |
- Dockerfile: Add USER cliproxy so mcpg and HTTP server run as non-root; fix log and TLS dir ownership to be writable by cliproxy before USER switch - entrypoint.sh: Fail closed when GH_TOKEN is missing (exit 1) instead of starting an unenforced server; fix signal handling by running Node in the background with wait/trap so cleanup kills both Node and mcpg on SIGTERM - server.js: Add blocked write actions for codespace (create/delete/edit/stop/ ports), cache (delete), and org (invite) in BLOCKED_ACTIONS_READONLY; track subcommandIndex during the scan instead of using args.indexOf() to prevent bypass when the subcommand string appears earlier as a flag value; add MAX_REQUEST_BODY_BYTES limit in readBody (returns null + 413 on overflow) - gh-cli-proxy-wrapper.sh: Remove curl -f; capture HTTP status code with -w and response body with -o so 4xx policy errors (403) surface the actual .error message instead of "CLI proxy unavailable" - cli.ts: Read GITHUB_TOKEN || GH_TOKEN for githubToken; update warning to mention both env var names - build.yml: Add 'Run CLI proxy unit tests' step parallel to api-proxy step - server.test.js: Add 11 new tests covering cache/codespace/org blocked actions and the subcommandIndex indexOf-bypass fix (58 tests total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/4d1512c6-a5af-49dc-9693-2f2c1af0b173
Applied all changes from the review in commit
|
Review: Phase 1 CLI Proxy SidecarExcellent implementation — the architecture closely follows the design doc, the test coverage is solid (49 server tests + 18 docker-manager tests), and the security hardening is thorough (cap_drop ALL, no-new-privileges, resource limits, execFile not shell). A few issues to address: 🔴 High:
|
|
@copilot apply changes based on the comments in #1730 (comment) |
- server.js: Remove 'api' from ALLOWED_SUBCOMMANDS_READONLY; raw HTTP passthrough can mutate via -X POST/PUT/DELETE. 'api' allowed in writable mode. Add explanatory comment. (61 tests total) - entrypoint.sh: Add --trusted-bots flag to mcpg so guard policies don't reject bot-authored content (Dependabot, Copilot, github-actions) - Dockerfile: Pin ghcr.io/github/gh-aw-mcpg :latest -> :v0.2.2 for reproducible builds - docker-manager.ts: Expand GITHUB_TOKEN exclusion comment explaining design intent and why it is safe in practice - cli.ts: Restore // -- Logging & Debug -- section header before --log-level - server.test.js: Add 3 new tests (61 total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/2e1f5421-3e78-44f2-a1c0-d2920f65a173
Applied all actionable items from the review in commit
|
|
@copilot make the gh-aw-mcpg container image an awf command-line parameter that the compiler needs to set. update the issue to reflec that the compiler will need to set this since it decides which mcpg version to pull and run. |
Make the gh-aw-mcpg source image configurable via CLI so the AWF compiler can control which mcpg version is pulled and run in the cli-proxy sidecar: - Dockerfile: use ARG MCPG_IMAGE (default ghcr.io/github/gh-aw-mcpg:v0.2.2) - src/types.ts: add cliProxyMcpgImage to WrapperConfig - src/cli.ts: add --cli-proxy-mcpg-image flag under CLI Proxy section - src/docker-manager.ts: pass MCPG_IMAGE build arg when building locally - src/docker-manager.test.ts: 3 new tests (320 total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/15ba6c64-db6c-4c47-b813-21878fb57168
Done in commit
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Security Review — PR #1730 (CLI proxy sidecar)Overall the PR is well-structured with good security foundations: 🔴 Flag-Parsing Bypass for
|
Add tests for: - resolveImages with enableCliProxy - resolveImages with both enableApiProxy and enableCliProxy - predownloadCommand with enableCliProxy - handlePredownloadAction forwarding enableCliProxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 86.20% | 86.10% | 📉 -0.10% |
| Statements | 86.07% | 85.97% | 📉 -0.10% |
| Functions | 87.41% | 87.41% | ➡️ +0.00% |
| Branches | 78.56% | 78.65% | 📈 +0.09% |
📁 Per-file Coverage Changes (2 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/cli.ts |
61.8% → 61.3% (-0.48%) | 62.3% → 61.8% (-0.47%) |
src/docker-manager.ts |
86.6% → 86.7% (+0.08%) | 86.1% → 86.2% (+0.10%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
Extract CLI proxy logging into testable emitCliProxyStatusLogs() function (same pattern as emitApiProxyTargetWarnings). Add tests for: - disabled/undefined cli proxy (no-op) - enabled with token present (read-only and writable modes) - enabled with missing token (warning messages) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
Smoke Test Results
Overall: PASS
|
🔥 Smoke Test Results
Overall: PASS (2/2 verifiable tests passed) PR: feat: phase 1 – gh CLI proxy sidecar with mcpg DIFC proxy
|
This comment has been minimized.
This comment has been minimized.
Smoke Test: GitHub Actions Services Connectivity ✅All checks passed against
|
Security Review — Action-Level Blocking BypassSeverity: Medium SummaryThe Vulnerable Code
const action = args.slice(subcommandIndex + 1).find(a => !a.startsWith('-'));
```
`find(a => !a.startsWith('-'))` returns the first argument that doesn't begin with `-`, which includes **flag values** (e.g., `owner/repo` in `--repo owner/repo`). When a flag with a non-flag-like value appears before the real action, the flag's value is mistakenly treated as the action.
### Proof-of-Concept Bypass
In read-only mode, `pr create` is in `BLOCKED_ACTIONS_READONLY`. However:
```
args = ['pr', '--repo', 'owner/repo', 'create']
The agent can then execute Affected blocked actions (non-exhaustive):
Missing Test CoverageNo test covers Suggested FixApply the same flag-skipping logic used for subcommand detection to the action scan: // Instead of:
const action = args.slice(subcommandIndex + 1).find(a => !a.startsWith('-'));
// Use:
function findFirstNonFlagArg(args) {
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg.startsWith('-')) {
// Skip flag and its value (if next arg is not a flag)
if (!arg.includes('=') && i + 1 < args.length && !args[i + 1].startsWith('-')) {
i += 2;
} else {
i += 1;
}
} else {
return arg;
}
}
return undefined;
}
const action = findFirstNonFlagArg(args.slice(subcommandIndex + 1));And add a regression test: it('should deny pr create when a flag with a value precedes the action', () => {
// gh pr --repo owner/repo create → must be blocked
const result = validateArgs(['pr', '--repo', 'owner/repo', 'create'], false);
expect(result.valid).toBe(false);
});Notes
|
Chroot Version Comparison Results
Overall: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot environment.
|
|
Smoke test matrix (run 24058930701):
|
Add build-cli-proxy job to release.yml following the same pattern as api-proxy: build+push to GHCR, cosign signing, SBOM generation, and SBOM attestation. Also update docs/releasing.md to document the new cli-proxy image. The predownload logic already includes cli-proxy support from PR #1730. Fixes #1746 Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/d6d42f21-06c7-45b4-8d7f-f60f1d597851 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
* Initial plan * fix: add cli-proxy container image to release workflow Add build-cli-proxy job to release.yml following the same pattern as api-proxy: build+push to GHCR, cosign signing, SBOM generation, and SBOM attestation. Also update docs/releasing.md to document the new cli-proxy image. The predownload logic already includes cli-proxy support from PR #1730. Fixes #1746 Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/d6d42f21-06c7-45b4-8d7f-f60f1d597851 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Agents currently access GitHub exclusively through the MCP protocol, which bloats context with 30+ tool schemas (~8–12k tokens/turn) and forces unnatural tool-call framing. The root constraint:
ghCLI requiresGH_TOKEN, which AWF deliberately excludes from the agent container.This implements Phase 1 of the CLI proxy sidecar design: a new
awf-cli-proxycontainer at172.30.0.50that runs an mcpg DIFC proxy (holdsGH_TOKEN, enforces guard policies) alongside a Node.js HTTP server. The agent gets a/usr/local/bin/ghwrapper that forwards invocations over HTTP — no token exposure, same DIFC guarantees.New container:
containers/cli-proxy/Dockerfile— Multi-stage: copiesmcpgbinary from a configurable image (defaultghcr.io/github/gh-aw-mcpg:v0.2.15, overridable via--cli-proxy-mcpg-image); layersghCLI + Node.js on Alpine; runs as non-root usercliproxyentrypoint.sh— Fails closed ifGH_TOKENis absent; starts mcpg proxy (--tls,--guards-mode filter,--trusted-bots github-actions[bot],github-actions,dependabot[bot],copilot, port127.0.0.1:18443), waits for TLS cert, then starts HTTP server under a supervisor loop (Node in background +wait) soSIGTERM/SIGINTgracefully stop both Node and mcpgserver.js—/exec(POST) and/health(GET); subcommand allowlist enforced with action-level blocks;execFile(not shell) prevents injection; configurable timeout + output cap; request body size limit (1 MB, returns 413 on overflow)server.test.js— 61 unit tests covering read-only/writable modes, flag-before-subcommand parsing, always-denied meta-commands,codespace/cache/orgwrite-action blocking, subcommand-index bypass prevention, andapiread-only denialRead-only by default (opt-in write via
--cli-proxy-writable):AWF integration
src/types.ts—CLI_PROXY_PORT = 11000; new config fields:enableCliProxy,cliProxyWritable,cliProxyPolicy,cliProxyMcpgImage,githubTokensrc/docker-manager.ts— cli-proxy service at172.30.0.50;AWF_CLI_PROXY_IPpropagated to iptables-init before service definition (same timing requirement as api-proxy);GITHUB_TOKEN/GH_TOKENexcluded from agent env when cli-proxy enabled (tokens held securely in mcpg; safe becauseactions/checkoutruns before awf and tools needingGITHUB_TOKENshould useGITHUB_MCP_SERVER_TOKEN); cli-proxy log preservation on cleanup;cliProxyIpadded to network config; passesMCPG_IMAGEDocker build arg when using--build-localsrc/cli.ts—--enable-cli-proxy,--cli-proxy-writable,--cli-proxy-policy,--cli-proxy-mcpg-imageflags; readsGITHUB_TOKEN || GH_TOKENfor the proxy token; warns when neither is set;predownloadsubcommand supports--enable-cli-proxycontainers/agent/setup-iptables.sh— NATRETURNrule forAWF_CLI_PROXY_IP(parallel to api-proxy pattern).github/workflows/build.yml— CLI proxy unit tests added to CI pipeline alongside the existing api-proxy test stepCompiler-controlled mcpg version
The AWF compiler (gh-aw) sets
--cli-proxy-mcpg-imageto control which mcpg version is pulled and run inside the cli-proxy sidecar. This is only used when building locally with--build-local; the pre-built GHCRcli-proxyimage already has mcpg bundled at release time. The Dockerfile usesARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.15so the default is deterministic and the compiler can override it for version pinning or testing new mcpg releases.# Compiler pins a specific mcpg version when building locally awf --build-local --enable-cli-proxy --cli-proxy-mcpg-image ghcr.io/github/gh-aw-mcpg:v0.3.0 ...Agent-side gh wrapper
gh-cli-proxy-wrapper.shinstalled in the agent image; activated at runtime (both chroot and non-chroot) whenAWF_CLI_PROXY_URLis set by copying to/tmp/awf-lib/ghand prepending toPATH. The wrapper captures HTTP status separately from the response body so 4xx policy errors (e.g. 403 on a blocked command) surface the actual error message rather than a generic "unavailable" message:Tests
docker-manager.test.tstests covering service generation, environment propagation, iptables-init env, writable mode, guard policy passthrough, image resolution,MCPG_IMAGEbuild arg passthrough, and agentdepends_oncontainers/cli-proxy/server.test.jstests, covering read-only/writable modes,codespace/cache/orgwrite-action blocking, subcommand-index indexOf-bypass fix, andapiread-only denialstartContainerstest to includeawf-cli-proxyin the pre-run container removal list