Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ The same concept applies to **Docker**:
- `AGENTFIELD_URL` (recommended): Control plane base URL.
- `AGENT_NODE_ID` (optional): Node id.
- `AGENT_CALLBACK_URL` (recommended in Docker/Kubernetes): URL the control plane will call back to (examples: `http://my-agent:8001`, or for host-run agents with Dockerized control plane: `http://host.docker.internal:8001`).
- `AGENTFIELD_SKIP_IP_DETECTION` (optional, default `false`): set to `true` to skip the automatic cloud-metadata and third-party IP lookups (`169.254.169.254`, `metadata.google.internal`, `api.ipify.org`) that always run when a container is detected, regardless of whether `AGENT_CALLBACK_URL` is already set. Set this when those probes generate egress traffic or network-policy-deny log noise you don't want.

Many Python examples also require model provider credentials (for example `OPENAI_API_KEY`), depending on the `AIConfig` you choose.

Expand Down
12 changes: 9 additions & 3 deletions sdk/python/agentfield/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ def _docstring_summary(func: Callable) -> str:
aiohttp = None


def _ip_detection_disabled() -> bool:
"""Whether cloud-metadata / third-party IP probing is opted out via env var."""
return os.getenv("AGENTFIELD_SKIP_IP_DETECTION", "false").lower() == "true"


def _detect_container_ip() -> Optional[str]:
"""
Detect the external IP address when running in a containerized environment.
Expand Down Expand Up @@ -367,9 +372,10 @@ def add_candidate(raw: Optional[str]):
if railway_service_name and railway_environment:
add_candidate(f"http://{railway_service_name}.railway.internal:{port}")

external_ip = _detect_container_ip()
if external_ip:
add_candidate(f"http://{external_ip}:{port}")
if not _ip_detection_disabled():
external_ip = _detect_container_ip()
if external_ip:
add_candidate(f"http://{external_ip}:{port}")

# 4. Local network hints
local_ip = _detect_local_ip()
Expand Down
47 changes: 47 additions & 0 deletions sdk/python/tests/test_agent_networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,53 @@ def test_resolve_callback_url_uses_first_candidate(monkeypatch):
assert resolved == "http://from-env:7777"


def test_build_callback_candidates_skips_ip_detection_when_disabled(monkeypatch):
"""AGENTFIELD_SKIP_IP_DETECTION=true must stop _detect_container_ip from being
called at all, not just from being used (issue #624: the cloud-metadata/ipify
probes fire unconditionally on every container start with no opt-out, flooding
NetworkPolicy deny logs and leaking pod egress for a best-effort candidate)."""
monkeypatch.setattr(agent_mod, "_is_running_in_container", lambda: True)
monkeypatch.setattr(agent_mod, "_detect_local_ip", lambda: None)
monkeypatch.setenv("AGENTFIELD_SKIP_IP_DETECTION", "true")

calls = []
monkeypatch.setattr(
agent_mod, "_detect_container_ip", lambda: calls.append(1) or "203.0.113.10"
)

candidates = _build_callback_candidates(None, 9090)

assert calls == []
assert not any("203.0.113.10" in candidate for candidate in candidates)


def test_build_callback_candidates_runs_ip_detection_by_default(monkeypatch):
"""Default behavior (no env var set) is unchanged: the probe still runs."""
monkeypatch.setattr(agent_mod, "_is_running_in_container", lambda: True)
monkeypatch.setattr(agent_mod, "_detect_local_ip", lambda: None)
monkeypatch.delenv("AGENTFIELD_SKIP_IP_DETECTION", raising=False)
monkeypatch.setattr(agent_mod, "_detect_container_ip", lambda: "203.0.113.10")

candidates = _build_callback_candidates(None, 9090)

assert any(candidate.startswith("http://203.0.113.10") for candidate in candidates)


@pytest.mark.parametrize("value", ["true", "True", "TRUE"])
def test_ip_detection_disabled_is_case_insensitive(monkeypatch, value):
monkeypatch.setenv("AGENTFIELD_SKIP_IP_DETECTION", value)
assert agent_mod._ip_detection_disabled() is True


@pytest.mark.parametrize("value", [None, "false", "0", "garbage"])
def test_ip_detection_disabled_defaults_to_false(monkeypatch, value):
if value is None:
monkeypatch.delenv("AGENTFIELD_SKIP_IP_DETECTION", raising=False)
else:
monkeypatch.setenv("AGENTFIELD_SKIP_IP_DETECTION", value)
assert agent_mod._ip_detection_disabled() is False


def test_build_callback_discovery_payload_marks_container(monkeypatch):
agent, _ = create_test_agent(monkeypatch)
agent.callback_candidates = ["http://first:7000", "http://second:7000"]
Expand Down