diff --git a/src/services/agent-environment-options.test.ts b/src/services/agent-environment-options.test.ts index 07b6c730f..68f9b3fc1 100644 --- a/src/services/agent-environment-options.test.ts +++ b/src/services/agent-environment-options.test.ts @@ -484,6 +484,21 @@ describe('agent environment: options', () => { expect(env.NO_PROXY).toContain('host.docker.internal'); }); + it('should append topology peer hostnames to NO_PROXY', () => { + const configWithTopologyPeers = { + ...mockConfig, + networkIsolation: true, + topologyAttach: ['awmg-mcpg', 'awmg-cli-proxy'], + }; + const result = generateDockerCompose(configWithTopologyPeers, mockNetworkConfig); + const agent = result.services.agent; + const env = agent.environment as Record; + + expect(env.NO_PROXY).toContain('awmg-mcpg'); + expect(env.NO_PROXY).toContain('awmg-cli-proxy'); + expect(env.no_proxy).toBe(env.NO_PROXY); + }); + it('should sync no_proxy when --env overrides NO_PROXY', () => { const configWithEnv = { ...mockConfig, diff --git a/src/services/agent-environment/proxy-environment.test.ts b/src/services/agent-environment/proxy-environment.test.ts index ffae83928..f2caa443b 100644 --- a/src/services/agent-environment/proxy-environment.test.ts +++ b/src/services/agent-environment/proxy-environment.test.ts @@ -35,4 +35,50 @@ describe('buildProxyEnvironment', () => { expect(env.NO_PROXY.split(',')).toContain('172.30.0.1'); expect(env.NO_PROXY.split(',')).toContain('host.docker.internal'); }); + + describe('topology-attached peers', () => { + it('exempts topology peers from proxy routing for a compose agent in isolation mode', () => { + const env = run({ + ...baseConfig, + networkIsolation: true, + topologyAttach: ['awmg-mcpg', 'awmg-cli-proxy'], + }); + expect(env.NO_PROXY.split(',')).toContain('awmg-mcpg'); + expect(env.NO_PROXY.split(',')).toContain('awmg-cli-proxy'); + expect(env.no_proxy).toBe(env.NO_PROXY); + }); + + it('exempts the DIFC/cli-proxy host even when not listed in topologyAttach', () => { + const env = run({ + ...baseConfig, + networkIsolation: true, + difcProxyHost: 'awmg-cli-proxy', + }); + expect(env.NO_PROXY.split(',')).toContain('awmg-cli-proxy'); + }); + + it('does NOT exempt topology peers outside network-isolation mode', () => { + const env = run({ + ...baseConfig, + networkIsolation: false, + topologyAttach: ['awmg-mcpg'], + }); + expect(env.NO_PROXY.split(',')).not.toContain('awmg-mcpg'); + }); + + it('does NOT exempt topology peers for a microVM (sbx) agent', () => { + // The sbx microVM runs off awf-net and cannot resolve or route to these + // container hostnames; NO_PROXY there would break the connection rather + // than fix it, so peers are deliberately omitted for microVM backends. + const env = run({ + ...baseConfig, + containerRuntime: 'sbx', + networkIsolation: true, + topologyAttach: ['awmg-mcpg'], + difcProxyHost: 'awmg-cli-proxy', + }); + expect(env.NO_PROXY.split(',')).not.toContain('awmg-mcpg'); + expect(env.NO_PROXY.split(',')).not.toContain('awmg-cli-proxy'); + }); + }); }); diff --git a/src/services/agent-environment/proxy-environment.ts b/src/services/agent-environment/proxy-environment.ts index 959ca435a..885153dfa 100644 --- a/src/services/agent-environment/proxy-environment.ts +++ b/src/services/agent-environment/proxy-environment.ts @@ -1,7 +1,7 @@ import { WrapperConfig } from '../../types'; import { NetworkConfig } from '../squid-service'; import { buildNoProxyValue } from '../no-proxy-utils'; -import { runtimeUsesIptables } from '../../container-runtime'; +import { runtimeUsesIptables, runtimeUsesComposeAgent } from '../../container-runtime'; interface ProxyEnvironmentParams { config: WrapperConfig; @@ -14,6 +14,29 @@ export function buildProxyEnvironment(params: ProxyEnvironmentParams): void { const noProxyHosts: string[] = ['0.0.0.0', networkConfig.squidIp, networkConfig.agentIp]; + // Network-isolation (topology) mode has no iptables-init container, so the + // only lever to keep intra-`awf-net` peer traffic out of Squid is NO_PROXY. + // Exempt topology-attached peers (e.g. the MCP gateway `awmg-mcpg` and the + // DIFC/cli-proxy `awmg-cli-proxy`) so proxy-aware clients (rmcp/undici) + // connect to them directly instead of being routed through Squid and denied. + // + // Only meaningful when the agent actually shares `awf-net`, i.e. a + // compose-managed agent (runc, gVisor). For microVM backends (Docker sbx) the + // agent runs off `awf-net` and cannot resolve or route to these container + // hostnames — adding them to NO_PROXY there would turn a Squid 403 into an + // unroutable direct connection, so they are deliberately skipped. sbx reaches + // gateway peers through its own proxy-chaining path, not NO_PROXY. + // + // Hostname-only by design: peer IPs are not known at config-write time (they + // are discovered after the network attach in cli-workflow's onNetworkReady), + // and MCP URLs address peers by hostname (e.g. http://awmg-mcpg:8080), which + // is what undici matches NO_PROXY against. + if (config.networkIsolation && runtimeUsesComposeAgent(config.containerRuntime)) { + if (config.topologyAttach) { + noProxyHosts.push(...config.topologyAttach); + } + } + // The MCP gateway is served on the network gateway (e.g. 172.30.0.1). In // standard mode an iptables NAT RETURN rule bypasses Squid for it. Runtimes // whose netstack can't use host-netns iptables (e.g. gVisor) have no such @@ -33,6 +56,6 @@ export function buildProxyEnvironment(params: ProxyEnvironmentParams): void { noProxyHosts.push(networkConfig.proxyIp, 'api-proxy'); } - environment.NO_PROXY = buildNoProxyValue(noProxyHosts); + environment.NO_PROXY = buildNoProxyValue([...new Set(noProxyHosts)]); environment.no_proxy = environment.NO_PROXY; }