Skip to content

Commit 93903cf

Browse files
CopilotlpcoxCopilot
authored
feat: phase 1 – gh CLI proxy sidecar with mcpg DIFC proxy (#1730)
* Initial plan * feat: implement Phase 1 - CLI proxy sidecar with mcpg DIFC proxy - 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: address code review and CodeQL feedback - 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 * fix: address all review feedback on cli-proxy - 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 * fix: api read-only deny, trusted-bots, pin mcpg image, comments - 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 * feat: add --cli-proxy-mcpg-image for compiler-controlled mcpg version 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 * feat: bump default mcpg image to v0.2.15 Update the default MCPG_IMAGE from v0.2.2 to v0.2.15 in: - containers/cli-proxy/Dockerfile (ARG default) - src/cli.ts (--cli-proxy-mcpg-image flag default) - src/types.ts (JSDoc @default) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/dba17693-9f82-440d-9284-d2e36ccac016 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * test: add coverage for enableCliProxy in predownload and CLI 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> * test: add coverage for emitCliProxyStatusLogs in cli.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> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 21616f5 commit 93903cf

19 files changed

Lines changed: 5971 additions & 7 deletions

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,9 @@ jobs:
6464
cd containers/api-proxy
6565
npm ci
6666
npm test
67+
68+
- name: Run CLI proxy unit tests
69+
run: |
70+
cd containers/cli-proxy
71+
npm ci
72+
npm test

containers/agent/Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \
7575
mkdir -p /home/awfuser/.copilot/logs && \
7676
chown -R awfuser:awfuser /home/awfuser
7777

78-
# Copy iptables setup script, PID logger, API proxy health check, and Claude key helper
78+
# Copy iptables setup script, PID logger, API proxy health check, Claude key helper,
79+
# and gh CLI proxy wrapper (used when --enable-cli-proxy is active)
7980
COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh
8081
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
8182
COPY pid-logger.sh /usr/local/bin/pid-logger.sh
8283
COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh
8384
COPY get-claude-key.sh /usr/local/bin/get-claude-key.sh
84-
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh
85+
COPY gh-cli-proxy-wrapper.sh /usr/local/bin/gh-cli-proxy-wrapper.sh
86+
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh /usr/local/bin/gh-cli-proxy-wrapper.sh
8587

8688
# Copy pre-built one-shot-token library from rust-builder stage
8789
# This prevents tokens from being read multiple times (e.g., by malicious code)

containers/agent/entrypoint.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,24 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
478478
fi
479479
fi
480480

481+
# Activate gh CLI proxy wrapper when CLI proxy sidecar is enabled.
482+
# The wrapper at /usr/local/bin/gh-cli-proxy-wrapper.sh (baked into the image)
483+
# is copied to /tmp/awf-lib/gh so it is accessible inside the chroot at a
484+
# location that takes precedence over the host's /usr/bin/gh mount.
485+
if [ -n "$AWF_CLI_PROXY_URL" ] && [ -f /usr/local/bin/gh-cli-proxy-wrapper.sh ]; then
486+
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
487+
if cp /usr/local/bin/gh-cli-proxy-wrapper.sh /host/tmp/awf-lib/gh 2>/dev/null && \
488+
chmod +x /host/tmp/awf-lib/gh 2>/dev/null; then
489+
# The chroot will see this as /tmp/awf-lib/gh (the /host prefix is the bind mount)
490+
echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh (inside chroot)"
491+
# Prepend /tmp/awf-lib to PATH so the wrapper takes precedence over host gh
492+
export AWF_HOST_PATH="/tmp/awf-lib:${AWF_HOST_PATH:-$PATH}"
493+
else
494+
echo "[entrypoint][WARN] Could not install gh CLI proxy wrapper"
495+
fi
496+
fi
497+
fi
498+
481499
# Copy AWF CA certificate to chroot-accessible path for ssl-bump TLS trust.
482500
# NODE_EXTRA_CA_CERTS points to /usr/local/share/ca-certificates/awf-ca.crt which
483501
# is a Docker volume mount on the container's overlay filesystem. After chroot /host,
@@ -782,6 +800,21 @@ AWFEOF
782800
else
783801
# Original behavior - run in container filesystem
784802
# Drop capabilities and privileges, then execute the user command
803+
804+
# Activate gh CLI proxy wrapper in non-chroot mode.
805+
# Copy the wrapper to /tmp/awf-lib/gh so it takes precedence over
806+
# the system gh at /usr/bin/gh (since /tmp/awf-lib is prepended to PATH).
807+
if [ -n "$AWF_CLI_PROXY_URL" ] && [ -f /usr/local/bin/gh-cli-proxy-wrapper.sh ]; then
808+
mkdir -p /tmp/awf-lib
809+
if cp /usr/local/bin/gh-cli-proxy-wrapper.sh /tmp/awf-lib/gh 2>/dev/null && \
810+
chmod +x /tmp/awf-lib/gh 2>/dev/null; then
811+
export PATH="/tmp/awf-lib:${PATH}"
812+
echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh"
813+
else
814+
echo "[entrypoint][WARN] Could not install gh CLI proxy wrapper"
815+
fi
816+
fi
817+
785818
# This prevents malicious code from modifying iptables rules or using chroot
786819
# Security note: capsh --drop removes capabilities from the bounding set,
787820
# preventing any process (even if it escalates to root) from acquiring them
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/bin/sh
2+
# /usr/local/bin/gh-cli-proxy-wrapper
3+
# Forwards gh CLI invocations to the CLI proxy sidecar over HTTP.
4+
# This wrapper is installed at /usr/local/bin/gh in the agent container
5+
# when --enable-cli-proxy is active, so it takes precedence over any
6+
# host-mounted gh binary at /host/usr/bin/gh.
7+
#
8+
# Dependencies: curl, jq (both available in the agent container)
9+
10+
CLI_PROXY="${AWF_CLI_PROXY_URL:-http://172.30.0.50:11000}"
11+
12+
# Build JSON array from all positional arguments
13+
ARGS_JSON='[]'
14+
if [ $# -gt 0 ]; then
15+
ARGS_JSON=$(printf '%s\n' "$@" | jq -R . | jq -s .)
16+
fi
17+
18+
# Capture working directory
19+
CWD=$(pwd)
20+
21+
# Read stdin if data is available (non-interactive)
22+
STDIN_DATA=""
23+
if [ ! -t 0 ]; then
24+
STDIN_DATA=$(cat | base64 | tr -d '\n')
25+
fi
26+
27+
# Use a temporary file to capture the response body without -f,
28+
# so we can read the body even on 4xx/5xx responses (e.g., 403 policy block).
29+
RESPONSE_FILE=$(mktemp)
30+
HTTP_STATUS=$(curl -s \
31+
--max-time 60 \
32+
-o "$RESPONSE_FILE" \
33+
-w "%{http_code}" \
34+
-X POST "${CLI_PROXY}/exec" \
35+
-H "Content-Type: application/json" \
36+
--data-binary "$(printf '{"args":%s,"cwd":%s,"stdin":"%s"}' \
37+
"$ARGS_JSON" \
38+
"$(printf '%s' "$CWD" | jq -Rs .)" \
39+
"$STDIN_DATA")")
40+
CURL_EXIT=$?
41+
RESPONSE=$(cat "$RESPONSE_FILE")
42+
rm -f "$RESPONSE_FILE"
43+
44+
if [ "$CURL_EXIT" -ne 0 ]; then
45+
echo "gh: CLI proxy unavailable at ${CLI_PROXY} (curl exit ${CURL_EXIT})" >&2
46+
exit 1
47+
fi
48+
49+
# Surface policy errors (403), request errors (400/413), and server errors (5xx)
50+
if [ "$HTTP_STATUS" != "200" ]; then
51+
ERROR=$(printf '%s' "$RESPONSE" | jq -r '.error // empty' 2>/dev/null)
52+
if [ -n "$ERROR" ]; then
53+
echo "gh: ${ERROR}" >&2
54+
else
55+
echo "gh: CLI proxy returned HTTP ${HTTP_STATUS}" >&2
56+
fi
57+
exit 1
58+
fi
59+
60+
# Extract and emit stdout/stderr from a successful 200 response
61+
STDOUT=$(printf '%s' "$RESPONSE" | jq -r '.stdout // empty' 2>/dev/null)
62+
STDERR=$(printf '%s' "$RESPONSE" | jq -r '.stderr // empty' 2>/dev/null)
63+
EXIT_CODE=$(printf '%s' "$RESPONSE" | jq -r '.exitCode // 1' 2>/dev/null)
64+
65+
printf '%s' "$STDOUT"
66+
printf '%s' "$STDERR" >&2
67+
exit "${EXIT_CODE:-1}"

containers/agent/setup-iptables.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ if [ -n "$AWF_API_PROXY_IP" ]; then
173173
iptables -t nat -A OUTPUT -d "$AWF_API_PROXY_IP" -j RETURN
174174
fi
175175

176+
# Allow traffic to CLI proxy sidecar (when enabled)
177+
# AWF_CLI_PROXY_IP is set by docker-manager.ts when --enable-cli-proxy is used
178+
if [ -n "$AWF_CLI_PROXY_IP" ]; then
179+
echo "[iptables] Allow traffic to CLI proxy sidecar (${AWF_CLI_PROXY_IP})..."
180+
iptables -t nat -A OUTPUT -d "$AWF_CLI_PROXY_IP" -j RETURN
181+
fi
182+
176183
# Validate port specification (single port 1-65535 or range N-M)
177184
# Rejects leading zeros (e.g., 080) to align with TypeScript isValidPortSpec()
178185
is_valid_port_spec() {

containers/cli-proxy/Dockerfile

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# CLI Proxy sidecar for AWF - provides gh CLI access with mcpg DIFC proxy
2+
#
3+
# Multi-stage build:
4+
# Stage 1: Extract mcpg binary from ghcr.io/github/gh-aw-mcpg image
5+
# Stage 2: Assemble final image with gh CLI, Node.js, and mcpg
6+
#
7+
# This container runs two processes:
8+
# 1. mcpg proxy (TLS, port 18443) - holds GH_TOKEN, enforces guard policies
9+
# 2. HTTP server (port 11000) - receives gh invocations from the agent container
10+
11+
# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image.
12+
# MCPG_IMAGE is configurable via --cli-proxy-mcpg-image so the AWF compiler
13+
# can control which mcpg version is pulled and run (e.g. for version pinning
14+
# or testing a new mcpg release before it is bundled in the GHCR cli-proxy image).
15+
ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.15
16+
FROM ${MCPG_IMAGE} AS mcpg-source
17+
18+
# Stage 2: Build the CLI proxy image
19+
FROM node:22-alpine
20+
21+
# Install gh CLI and curl for healthchecks/wrapper
22+
# gh CLI is available in the Alpine community repository
23+
RUN apk add --no-cache \
24+
curl \
25+
github-cli \
26+
ca-certificates \
27+
bash
28+
29+
# Copy the mcpg binary from the mcpg-source stage
30+
COPY --from=mcpg-source /usr/local/bin/mcpg /usr/local/bin/mcpg
31+
RUN chmod +x /usr/local/bin/mcpg
32+
33+
# Create app directory
34+
WORKDIR /app
35+
36+
# Copy package files
37+
COPY package*.json ./
38+
39+
# Install dependencies from lockfile (deterministic)
40+
RUN npm ci --omit=dev
41+
42+
# Copy application files
43+
COPY server.js ./
44+
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
45+
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
46+
47+
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh
48+
49+
# Create non-root user
50+
RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy
51+
52+
# Create log directory owned by cliproxy (so non-root process can write)
53+
RUN mkdir -p /var/log/cli-proxy/mcpg && \
54+
chown -R cliproxy:cliproxy /var/log/cli-proxy
55+
56+
# Create /tmp/proxy-tls directory owned by cliproxy for mcpg TLS cert generation
57+
RUN mkdir -p /tmp/proxy-tls && chown cliproxy:cliproxy /tmp/proxy-tls
58+
59+
# Switch to non-root user
60+
USER cliproxy
61+
62+
# Expose port for agent→cli-proxy HTTP communication
63+
# 11000 - gh exec endpoint and health check
64+
EXPOSE 11000
65+
66+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

containers/cli-proxy/entrypoint.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/bin/bash
2+
# CLI Proxy sidecar entrypoint
3+
# Starts the mcpg DIFC proxy (GH_TOKEN required), then starts the Node.js HTTP server
4+
# under a supervisor loop so signals are properly handled and mcpg is cleaned up.
5+
set -e
6+
7+
echo "[cli-proxy] Starting CLI proxy sidecar..."
8+
9+
MCPG_PID=""
10+
NODE_PID=""
11+
12+
# GH_TOKEN is required: without it, mcpg cannot authenticate and DIFC guard policies
13+
# cannot be enforced. Fail closed rather than starting an unenforced server.
14+
if [ -z "$GH_TOKEN" ]; then
15+
echo "[cli-proxy] ERROR: GH_TOKEN not set - refusing to start without mcpg DIFC enforcement"
16+
exit 1
17+
fi
18+
19+
echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..."
20+
21+
mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg
22+
23+
# Build the guard policy JSON if not explicitly provided
24+
if [ -z "$AWF_GH_GUARD_POLICY" ]; then
25+
if [ -n "$GITHUB_REPOSITORY" ]; then
26+
AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}"
27+
else
28+
AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}"
29+
fi
30+
echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}"
31+
else
32+
echo "[cli-proxy] Using provided guard policy"
33+
fi
34+
35+
# Start mcpg proxy in background
36+
# mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding
37+
mcpg proxy \
38+
--policy "${AWF_GH_GUARD_POLICY}" \
39+
--listen 127.0.0.1:18443 \
40+
--tls \
41+
--tls-dir /tmp/proxy-tls \
42+
--guards-mode filter \
43+
--trusted-bots "github-actions[bot],github-actions,dependabot[bot],copilot" \
44+
--log-dir /var/log/cli-proxy/mcpg &
45+
MCPG_PID=$!
46+
echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})"
47+
48+
# Wait for TLS cert to be generated (max 30s)
49+
echo "[cli-proxy] Waiting for mcpg TLS certificate..."
50+
i=0
51+
while [ $i -lt 30 ]; do
52+
if [ -f /tmp/proxy-tls/ca.crt ]; then
53+
echo "[cli-proxy] TLS certificate available"
54+
break
55+
fi
56+
sleep 1
57+
i=$((i + 1))
58+
done
59+
60+
if [ ! -f /tmp/proxy-tls/ca.crt ]; then
61+
echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s"
62+
kill "$MCPG_PID" 2>/dev/null || true
63+
exit 1
64+
fi
65+
66+
# Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA)
67+
export GH_HOST="localhost:18443"
68+
export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt"
69+
export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}"
70+
71+
echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}"
72+
73+
# Cleanup handler: stop both the Node HTTP server and mcpg when we receive a signal
74+
# or when the server exits. This runs correctly because we do NOT exec Node — we
75+
# start it in the background and wait, so the shell (and its traps) remain active.
76+
cleanup() {
77+
echo "[cli-proxy] Shutting down..."
78+
if [ -n "$NODE_PID" ]; then
79+
kill "$NODE_PID" 2>/dev/null || true
80+
wait "$NODE_PID" 2>/dev/null || true
81+
fi
82+
if [ -n "$MCPG_PID" ]; then
83+
kill "$MCPG_PID" 2>/dev/null || true
84+
wait "$MCPG_PID" 2>/dev/null || true
85+
fi
86+
}
87+
trap 'cleanup; exit 0' INT TERM
88+
89+
# Start the Node.js HTTP server in the background so the shell keeps running
90+
# and traps remain active for graceful shutdown.
91+
echo "[cli-proxy] Starting HTTP server on port 11000..."
92+
node /app/server.js &
93+
NODE_PID=$!
94+
95+
# Wait for Node to exit and propagate its exit code
96+
if wait "$NODE_PID"; then
97+
NODE_EXIT=0
98+
else
99+
NODE_EXIT=$?
100+
fi
101+
102+
cleanup
103+
exit "$NODE_EXIT"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
# Healthcheck for the CLI proxy sidecar
3+
# Verifies the HTTP server is responsive
4+
curl -sf --max-time 3 http://localhost:11000/health > /dev/null

0 commit comments

Comments
 (0)