Skip to content

Commit 87f7028

Browse files
Mossakaclaude
andcommitted
fix: fix API proxy sidecar bugs preventing Anthropic-only usage
The API proxy sidecar (PR #751) had several bugs that prevented it from working when only an Anthropic API key was provided: 1. Health endpoint only listened on port 10000 when OpenAI key was set, causing Docker healthcheck failures with Anthropic-only configs 2. http-proxy-middleware didn't route through Squid (no HTTP_PROXY support), replaced with https-proxy-agent for explicit Squid routing 3. Missing package-lock.json caused npm ci to fail during container build 4. Host-level iptables (DOCKER-USER) blocked agent→sidecar traffic 5. Agent iptables OUTPUT filter dropped TCP to sidecar IP 6. NO_PROXY not set, causing curl to route sidecar requests through Squid 7. Docker DNS couldn't resolve 'api-proxy' hostname in chroot mode, switched BASE_URLs to use IP addresses directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 766e466 commit 87f7028

11 files changed

Lines changed: 396 additions & 72 deletions

File tree

containers/agent/setup-iptables.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ fi
127127
echo "[iptables] Allow traffic to Squid proxy (${SQUID_IP}:${SQUID_PORT})..."
128128
iptables -t nat -A OUTPUT -d "$SQUID_IP" -j RETURN
129129

130+
# Allow traffic to API proxy sidecar (when enabled)
131+
# AWF_API_PROXY_IP is set by docker-manager.ts when --enable-api-proxy is used
132+
if [ -n "$AWF_API_PROXY_IP" ]; then
133+
echo "[iptables] Allow traffic to API proxy sidecar (${AWF_API_PROXY_IP})..."
134+
iptables -t nat -A OUTPUT -d "$AWF_API_PROXY_IP" -j RETURN
135+
fi
136+
130137
# Bypass Squid for host.docker.internal when host access is enabled.
131138
# MCP gateway traffic to host.docker.internal gets DNAT'd to Squid,
132139
# where Squid fails with "Invalid URL" because rmcp sends relative URLs.
@@ -263,6 +270,11 @@ iptables -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j ACCEPT
263270
# Allow traffic to Squid proxy (after NAT redirection)
264271
iptables -A OUTPUT -p tcp -d "$SQUID_IP" -j ACCEPT
265272

273+
# Allow traffic to API proxy sidecar (when enabled)
274+
if [ -n "$AWF_API_PROXY_IP" ]; then
275+
iptables -A OUTPUT -p tcp -d "$AWF_API_PROXY_IP" -j ACCEPT
276+
fi
277+
266278
# Drop all other TCP traffic (default deny policy)
267279
# This ensures that only explicitly allowed ports can be accessed
268280
echo "[iptables] Drop all non-redirected TCP traffic (default deny)..."

containers/api-proxy/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ WORKDIR /app
1111
# Copy package files
1212
COPY package*.json ./
1313

14-
# Install dependencies
15-
RUN npm ci --only=production
14+
# Install dependencies (generates lockfile in container)
15+
RUN npm install --omit=dev
1616

1717
# Copy application files
1818
COPY server.js ./

containers/api-proxy/package-lock.json

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

containers/api-proxy/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
"start": "node server.js"
88
},
99
"dependencies": {
10-
"express": "^4.18.2",
11-
"http-proxy-middleware": "^2.0.6"
10+
"https-proxy-agent": "^7.0.6"
1211
},
1312
"engines": {
1413
"node": ">=18.0.0"

containers/api-proxy/server.js

Lines changed: 109 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@
1010
* 4. Respects domain whitelisting enforced by Squid
1111
*/
1212

13-
const express = require('express');
14-
const { createProxyMiddleware } = require('http-proxy-middleware');
13+
const http = require('http');
14+
const https = require('https');
15+
const { URL } = require('url');
16+
const { HttpsProxyAgent } = require('https-proxy-agent');
1517

1618
// Read API keys from environment (set by docker-compose)
1719
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
1820
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1921

2022
// Squid proxy configuration (set via HTTP_PROXY/HTTPS_PROXY in docker-compose)
21-
const HTTP_PROXY = process.env.HTTP_PROXY;
22-
const HTTPS_PROXY = process.env.HTTPS_PROXY;
23+
const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
2324

2425
console.log('[API Proxy] Starting AWF API proxy sidecar...');
25-
console.log(`[API Proxy] HTTP_PROXY: ${HTTP_PROXY}`);
2626
console.log(`[API Proxy] HTTPS_PROXY: ${HTTPS_PROXY}`);
2727
if (OPENAI_API_KEY) {
2828
console.log('[API Proxy] OpenAI API key configured');
@@ -31,72 +31,126 @@ if (ANTHROPIC_API_KEY) {
3131
console.log('[API Proxy] Anthropic API key configured');
3232
}
3333

34-
// Create Express app
35-
const app = express();
36-
37-
// Health check endpoint
38-
app.get('/health', (req, res) => {
39-
res.status(200).json({
40-
status: 'healthy',
41-
service: 'awf-api-proxy',
42-
squid_proxy: HTTP_PROXY || 'not configured',
43-
providers: {
44-
openai: !!OPENAI_API_KEY,
45-
anthropic: !!ANTHROPIC_API_KEY
34+
// Create proxy agent for routing through Squid
35+
const proxyAgent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY) : undefined;
36+
if (!proxyAgent) {
37+
console.warn('[API Proxy] WARNING: No HTTPS_PROXY configured, requests will go direct');
38+
}
39+
40+
/**
41+
* Forward a request to the target API, injecting auth headers and routing through Squid.
42+
*/
43+
function proxyRequest(req, res, targetHost, injectHeaders) {
44+
// Build target URL
45+
const targetUrl = new URL(req.url, `https://${targetHost}`);
46+
47+
// Read the request body
48+
const chunks = [];
49+
req.on('data', chunk => chunks.push(chunk));
50+
req.on('end', () => {
51+
const body = Buffer.concat(chunks);
52+
53+
// Copy incoming headers, inject auth headers
54+
const headers = { ...req.headers };
55+
delete headers.host; // Replace with target host
56+
Object.assign(headers, injectHeaders);
57+
58+
const options = {
59+
hostname: targetHost,
60+
port: 443,
61+
path: targetUrl.pathname + targetUrl.search,
62+
method: req.method,
63+
headers,
64+
agent: proxyAgent, // Route through Squid
65+
};
66+
67+
const proxyReq = https.request(options, (proxyRes) => {
68+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
69+
proxyRes.pipe(res);
70+
});
71+
72+
proxyReq.on('error', (err) => {
73+
console.error(`[API Proxy] Error proxying to ${targetHost}: ${err.message}`);
74+
if (!res.headersSent) {
75+
res.writeHead(502, { 'Content-Type': 'application/json' });
76+
}
77+
res.end(JSON.stringify({ error: 'Proxy error', message: err.message }));
78+
});
79+
80+
if (body.length > 0) {
81+
proxyReq.write(body);
4682
}
83+
proxyReq.end();
4784
});
48-
});
85+
}
86+
87+
// Health port is always 10000 — this is what Docker healthcheck hits
88+
const HEALTH_PORT = 10000;
4989

5090
// OpenAI API proxy (port 10000)
5191
if (OPENAI_API_KEY) {
52-
app.use(createProxyMiddleware({
53-
target: 'https://api.openai.com',
54-
changeOrigin: true,
55-
secure: true,
56-
onProxyReq: (proxyReq, req, res) => {
57-
// Inject Authorization header
58-
proxyReq.setHeader('Authorization', `Bearer ${OPENAI_API_KEY}`);
59-
console.log(`[OpenAI Proxy] ${req.method} ${req.url}`);
60-
},
61-
onError: (err, req, res) => {
62-
console.error(`[OpenAI Proxy] Error: ${err.message}`);
63-
res.status(502).json({ error: 'Proxy error', message: err.message });
92+
const server = http.createServer((req, res) => {
93+
if (req.url === '/health' && req.method === 'GET') {
94+
res.writeHead(200, { 'Content-Type': 'application/json' });
95+
res.end(JSON.stringify({
96+
status: 'healthy',
97+
service: 'awf-api-proxy',
98+
squid_proxy: HTTPS_PROXY || 'not configured',
99+
providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY },
100+
}));
101+
return;
102+
}
103+
104+
console.log(`[OpenAI Proxy] ${req.method} ${req.url}`);
105+
proxyRequest(req, res, 'api.openai.com', {
106+
'Authorization': `Bearer ${OPENAI_API_KEY}`,
107+
});
108+
});
109+
110+
server.listen(HEALTH_PORT, '0.0.0.0', () => {
111+
console.log(`[API Proxy] OpenAI proxy listening on port ${HEALTH_PORT}`);
112+
});
113+
} else {
114+
// No OpenAI key — still need a health endpoint on port 10000 for Docker healthcheck
115+
const server = http.createServer((req, res) => {
116+
if (req.url === '/health' && req.method === 'GET') {
117+
res.writeHead(200, { 'Content-Type': 'application/json' });
118+
res.end(JSON.stringify({
119+
status: 'healthy',
120+
service: 'awf-api-proxy',
121+
squid_proxy: HTTPS_PROXY || 'not configured',
122+
providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY },
123+
}));
124+
return;
64125
}
65-
}));
66126

67-
app.listen(10000, '0.0.0.0', () => {
68-
console.log('[API Proxy] OpenAI proxy listening on port 10000');
69-
console.log('[API Proxy] Routing through Squid to api.openai.com');
127+
res.writeHead(404, { 'Content-Type': 'application/json' });
128+
res.end(JSON.stringify({ error: 'OpenAI proxy not configured (no OPENAI_API_KEY)' }));
129+
});
130+
131+
server.listen(HEALTH_PORT, '0.0.0.0', () => {
132+
console.log(`[API Proxy] Health endpoint listening on port ${HEALTH_PORT} (OpenAI not configured)`);
70133
});
71134
}
72135

73136
// Anthropic API proxy (port 10001)
74137
if (ANTHROPIC_API_KEY) {
75-
const anthropicApp = express();
138+
const server = http.createServer((req, res) => {
139+
if (req.url === '/health' && req.method === 'GET') {
140+
res.writeHead(200, { 'Content-Type': 'application/json' });
141+
res.end(JSON.stringify({ status: 'healthy', service: 'anthropic-proxy' }));
142+
return;
143+
}
76144

77-
anthropicApp.get('/health', (req, res) => {
78-
res.status(200).json({ status: 'healthy', service: 'anthropic-proxy' });
145+
console.log(`[Anthropic Proxy] ${req.method} ${req.url}`);
146+
proxyRequest(req, res, 'api.anthropic.com', {
147+
'x-api-key': ANTHROPIC_API_KEY,
148+
'anthropic-version': '2023-06-01',
149+
});
79150
});
80151

81-
anthropicApp.use(createProxyMiddleware({
82-
target: 'https://api.anthropic.com',
83-
changeOrigin: true,
84-
secure: true,
85-
onProxyReq: (proxyReq, req, res) => {
86-
// Inject Anthropic authentication headers
87-
proxyReq.setHeader('x-api-key', ANTHROPIC_API_KEY);
88-
proxyReq.setHeader('anthropic-version', '2023-06-01');
89-
console.log(`[Anthropic Proxy] ${req.method} ${req.url}`);
90-
},
91-
onError: (err, req, res) => {
92-
console.error(`[Anthropic Proxy] Error: ${err.message}`);
93-
res.status(502).json({ error: 'Proxy error', message: err.message });
94-
}
95-
}));
96-
97-
anthropicApp.listen(10001, '0.0.0.0', () => {
152+
server.listen(10001, '0.0.0.0', () => {
98153
console.log('[API Proxy] Anthropic proxy listening on port 10001');
99-
console.log('[API Proxy] Routing through Squid to api.anthropic.com');
100154
});
101155
}
102156

src/cli-workflow.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ export async function runMainWorkflow(
4343
logger.info('Setting up host-level firewall network and iptables rules...');
4444
const networkConfig = await dependencies.ensureFirewallNetwork();
4545
const dnsServers = config.dnsServers || ['8.8.8.8', '8.8.4.4'];
46-
// API proxy (when enabled) does NOT get a firewall exemption - it routes through Squid
47-
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers);
46+
// When API proxy is enabled, allow agent→sidecar traffic at the host level.
47+
// The sidecar itself routes through Squid, so domain whitelisting is still enforced.
48+
const apiProxyIp = config.enableApiProxy ? networkConfig.proxyIp : undefined;
49+
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers, apiProxyIp);
4850
onHostIptablesSetup?.();
4951

5052
// Step 1: Write configuration files

src/docker-manager.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,7 +1550,7 @@ describe('docker-manager', () => {
15501550
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
15511551
const agent = result.services.agent;
15521552
const env = agent.environment as Record<string, string>;
1553-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1553+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
15541554
});
15551555

15561556
it('should configure HTTP_PROXY and HTTPS_PROXY in api-proxy to route through Squid', () => {
@@ -1567,16 +1567,16 @@ describe('docker-manager', () => {
15671567
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
15681568
const agent = result.services.agent;
15691569
const env = agent.environment as Record<string, string>;
1570-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1570+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
15711571
});
15721572

15731573
it('should set both BASE_URL variables when both keys are provided', () => {
15741574
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key', anthropicApiKey: 'sk-ant-test-key' };
15751575
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
15761576
const agent = result.services.agent;
15771577
const env = agent.environment as Record<string, string>;
1578-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1579-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1578+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
1579+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
15801580
});
15811581

15821582
it('should not set OPENAI_BASE_URL in agent when only Anthropic key is provided', () => {
@@ -1585,7 +1585,7 @@ describe('docker-manager', () => {
15851585
const agent = result.services.agent;
15861586
const env = agent.environment as Record<string, string>;
15871587
expect(env.OPENAI_BASE_URL).toBeUndefined();
1588-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1588+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
15891589
});
15901590

15911591
it('should not set ANTHROPIC_BASE_URL in agent when only OpenAI key is provided', () => {
@@ -1594,7 +1594,7 @@ describe('docker-manager', () => {
15941594
const agent = result.services.agent;
15951595
const env = agent.environment as Record<string, string>;
15961596
expect(env.ANTHROPIC_BASE_URL).toBeUndefined();
1597-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1597+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
15981598
});
15991599
});
16001600
});

0 commit comments

Comments
 (0)