Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/services/agent-environment-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;

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,
Expand Down
46 changes: 46 additions & 0 deletions src/services/agent-environment/proxy-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
27 changes: 25 additions & 2 deletions src/services/agent-environment/proxy-environment.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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;
}
Loading