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
12 changes: 8 additions & 4 deletions docs/reference/mcp-deployment-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,15 +320,19 @@ that performs four probes and returns a structured allow/deny matrix.
"gateway_reachable": true,
"internet_blocked": true,
"agent_pods_unreachable": true,
"orchestrator_direct_blocked": true,
"orchestrator_api_reachable": true,
"probe_job": "egg-probe-<uuid>",
"probe_pod_phase": "Succeeded"
}
```

All four boolean fields should be `true` for a correctly isolated agent
(the agent can reach the gateway for proxied API calls, nothing else).
Any `false` indicates a NetworkPolicy regression.
(the agent can reach the gateway for proxied API calls and heartbeat the
orchestrator on `:9849`; nothing else). Any `false` indicates a
NetworkPolicy regression. `orchestrator_api_reachable` was previously
named `orchestrator_direct_blocked` with inverted polarity, which read
backwards from intent — `allow-agent-to-orchestrator` deliberately
permits the heartbeat path (#2652).

**Probe Job design** (RISK-1 mitigation):

Expand Down Expand Up @@ -370,7 +374,7 @@ result = await mcp.call_tool("validate_network_isolation", {
assert result["gateway_reachable"] is True
assert result["internet_blocked"] is True
assert result["agent_pods_unreachable"] is True
assert result["orchestrator_direct_blocked"] is True
assert result["orchestrator_api_reachable"] is True
```

The expected allow/deny matrix is documented in
Expand Down
50 changes: 50 additions & 0 deletions integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ class EggStack(GatewayClientMixin):
gateway_port: int
proxy_port: int
launcher_secret: str
# Lifecycle bearer for the orchestrator's /api/v1/deployment/* and
# other ``@require_lifecycle_secret`` routes. Sourced from the same
# gateway-secrets Secret the orchestrator pod mounts; empty when the
# cluster has no lifecycle-secret key, so deployment-route tests can
# skip rather than fail closed.
lifecycle_secret: str
# Under k3s this carries the ``k8s-<namespace>`` sentinel — legacy
# docker-only fixtures key off the prefix to skip cleanly. Some tests
# (e.g. test_stack_lifecycle, test_worktree_integration) still consume
Expand Down Expand Up @@ -271,6 +277,31 @@ def _k8s_egg_stack() -> Generator[EggStack]:
else:
launcher_secret = os.environ.get("EGG_LAUNCHER_SECRET", secrets.token_urlsafe(32))

# Pull the lifecycle bearer from the same Secret so tests targeting
# ``@require_lifecycle_secret`` routes (e.g. /api/v1/deployment/*)
# can authenticate. Optional: if the cluster doesn't expose this
# key the bearer is left empty and callers should skip cleanly.
lifecycle_result = subprocess.run(
[
"kubectl",
"-n",
"egg-system",
"get",
"secret",
"gateway-secrets",
"-o",
"jsonpath={.data.lifecycle-secret}",
],
capture_output=True,
text=True,
timeout=10,
check=False,
)
if lifecycle_result.returncode == 0 and lifecycle_result.stdout:
lifecycle_secret = base64.b64decode(lifecycle_result.stdout).decode().strip()
else:
lifecycle_secret = ""

config_dir = tempfile.mkdtemp(prefix="egg-test-config-")
_write_test_config(config_dir, launcher_secret)

Expand All @@ -285,6 +316,7 @@ def _k8s_egg_stack() -> Generator[EggStack]:
gateway_port=int(gateway_port_str),
proxy_port=PROXY_PORT,
launcher_secret=launcher_secret,
lifecycle_secret=lifecycle_secret,
compose_project=f"k8s-{test_namespace}",
config_dir=config_dir,
isolated_network=test_namespace,
Expand Down Expand Up @@ -327,6 +359,24 @@ def orchestrator_url(egg_stack: EggStack) -> str:
return egg_stack.orchestrator_url


@pytest.fixture(scope="session")
def lifecycle_secret(egg_stack: EggStack) -> str:
"""Lifecycle bearer for orchestrator `@require_lifecycle_secret` routes.

Skips the test when the cluster's ``gateway-secrets`` Secret has no
``lifecycle-secret`` key — auth-required routes can't be exercised
without it, and the auth-reject suite in
``test_k8s_deployment_tools.py`` already covers the missing-secret
failure mode.
"""
if not egg_stack.lifecycle_secret:
pytest.skip(
"no lifecycle-secret key in gateway-secrets — auth-required "
"deployment-route tests need it"
)
return egg_stack.lifecycle_secret


@pytest.fixture(scope="session")
def orchestrator_mcp_url(egg_stack: EggStack) -> str:
"""Streamable-HTTP URL for the orchestrator's MCP server.
Expand Down
Loading
Loading