Skip to content

Commit d6abd70

Browse files
alexsteeelclaude
andcommitted
feat: generate unique Docker network subnets per worktree
- Add NETWORK_SUBNET and DNS_PROXY_IP environment variables to docker-compose.base.yaml - Generate deterministic /24 subnets based on MD5 hash of directory name - Use directory name (not config.name) to ensure each worktree gets unique subnet - Fixes "Pool overlaps with other one on this address space" error when running multiple worktrees 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 62b2c28 commit d6abd70

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-6
lines changed

src/ai_sbx/commands/init.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,10 +1399,20 @@ def project_setup_impl(
13991399
env_file = path / ".devcontainer" / ".env"
14001400
if not env_file.exists():
14011401
env_file.parent.mkdir(parents=True, exist_ok=True)
1402+
1403+
# Generate unique subnet for this worktree to avoid network conflicts
1404+
from ai_sbx.templates import generate_unique_subnet
1405+
1406+
subnet, dns_ip = generate_unique_subnet(path.name)
1407+
14021408
env_content = f"""# Project environment variables
14031409
PROJECT_NAME={path.name}
14041410
PROJECT_DIR={path}
14051411
COMPOSE_PROJECT_NAME={path.name}
1412+
1413+
# Network configuration (unique per worktree to avoid conflicts)
1414+
NETWORK_SUBNET={subnet}
1415+
DNS_PROXY_IP={dns_ip}
14061416
"""
14071417
env_file.write_text(env_content)
14081418
console.print("[green]✓[/green] Created .env file")

src/ai_sbx/docker-compose.base.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ services:
5151
restart: unless-stopped
5252
networks:
5353
ai-sbx-internal:
54-
ipv4_address: 172.28.0.53 # Static IP for DNS (port 53 reference)
54+
ipv4_address: ${DNS_PROXY_IP:-172.28.0.53} # Static IP for DNS (port 53 reference)
5555
ai-sbx-external:
5656

5757
# Main proxy for devcontainer - supports upstream proxy
@@ -77,7 +77,7 @@ services:
7777
# Use internal DNS proxy for name resolution
7878
# This allows DNS to work while keeping container on internal network only
7979
dns:
80-
- 172.28.0.53
80+
- ${DNS_PROXY_IP:-172.28.0.53}
8181
environment:
8282
<<: *devcontainer-proxy-env
8383
# Project-specific environment
@@ -158,7 +158,7 @@ networks:
158158
internal: true # Critical: blocks direct internet access
159159
ipam:
160160
config:
161-
- subnet: 172.28.0.0/16
161+
- subnet: ${NETWORK_SUBNET:-172.28.0.0/16}
162162
ai-sbx-external:
163163
name: ${PROJECT_NAME:-project}-ai-sbx-external
164164
driver: bridge

src/ai_sbx/templates.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,43 @@
11
"""Template management for AI Agents Sandbox."""
22

3+
import hashlib
34
from pathlib import Path
4-
from typing import Optional
5+
from typing import Optional, Tuple
56

67
from jinja2 import Environment, FileSystemLoader, Template
78

89
from ai_sbx.config import BaseImage, ProjectConfig, get_default_whitelist_domains
910
from ai_sbx.utils import logger
1011

1112

13+
def generate_unique_subnet(project_name: str) -> Tuple[str, str]:
14+
"""Generate a unique subnet and DNS IP based on project name.
15+
16+
Uses a hash of the project name to generate a deterministic subnet
17+
in the 172.16-31.x.0/24 range to avoid conflicts with other worktrees.
18+
19+
Args:
20+
project_name: The project name to hash
21+
22+
Returns:
23+
Tuple of (subnet, dns_ip) e.g. ("172.18.42.0/24", "172.18.42.53")
24+
"""
25+
# Create a hash of the project name
26+
hash_bytes = hashlib.md5(project_name.encode()).digest()
27+
28+
# Use first two bytes for second and third octet
29+
# Second octet: 16-31 (16 values) to stay in private 172.16.0.0/12 range
30+
second_octet = 16 + (hash_bytes[0] % 16)
31+
# Third octet: 0-255
32+
third_octet = hash_bytes[1]
33+
34+
# Total unique subnets: 16 * 256 = 4096 different /24 subnets
35+
subnet = f"172.{second_octet}.{third_octet}.0/24"
36+
dns_ip = f"172.{second_octet}.{third_octet}.53"
37+
38+
return subnet, dns_ip
39+
40+
1241
def get_docker_image_name(base_image: BaseImage) -> str:
1342
"""Map base image type to actual Docker image name."""
1443
mapping = {
@@ -295,13 +324,26 @@ def _generate_dockerfile(self, config: ProjectConfig) -> str:
295324

296325
def _generate_env_file(self, config: ProjectConfig) -> str:
297326
"""Generate .env file content with only Docker runtime variables."""
327+
# Use directory name (not config.name) for unique identification
328+
# This ensures each worktree gets unique network settings
329+
# even though they share the same ai-sbx.yaml with same config.name
330+
dir_name = config.path.name
331+
332+
# Generate unique subnet for this project/worktree to avoid network conflicts
333+
subnet, dns_ip = generate_unique_subnet(dir_name)
334+
298335
template = """# Docker Compose Runtime Configuration
299336
# This file is auto-generated from ai-sbx.yaml
300337
# To modify settings, edit ai-sbx.yaml and run 'ai-sbx init update'
301338
302339
# Required for Docker Compose
303340
PROJECT_DIR={{ config.path }}
304-
COMPOSE_PROJECT_NAME={{ config.name }}
341+
PROJECT_NAME={{ dir_name }}
342+
COMPOSE_PROJECT_NAME={{ dir_name }}
343+
344+
# Network configuration (unique per project/worktree to avoid conflicts)
345+
NETWORK_SUBNET={{ subnet }}
346+
DNS_PROXY_IP={{ dns_ip }}
305347
306348
# Docker image version
307349
IMAGE_TAG={{ config.docker.image_tag }}
@@ -330,7 +372,7 @@ def _generate_env_file(self, config: ProjectConfig) -> str:
330372
{{ key }}={{ value }}
331373
{% endfor -%}
332374
"""
333-
return Template(template).render(config=config)
375+
return Template(template).render(config=config, dir_name=dir_name, subnet=subnet, dns_ip=dns_ip)
334376

335377
def _generate_whitelist(self, config: ProjectConfig) -> str:
336378
"""Generate whitelist.txt content."""

0 commit comments

Comments
 (0)