plane(2.3): egress -- one TLS rule over store host, Git hosts and Model hosts; no http rule

policy_for(task, workspaces, model, store_host, receiver_host=None) emits one
tlsPassthrough rule on 443 over the sorted union of the memory store's host,
the bound Workspaces' https Git hosts and the bound Model's egress hosts. There
is no http rule: an http Git repo is refused by name (it could not be reached),
a store given as an address is refused naming S3_ENDPOINT (the gateway allows
hostnames only). receiver_host adds the old receiver's http rule for the path
group 2 keeps until 2.6; the reconciler passes it by keyword (passing the
receiver's address positionally would have raised outside the worker's try and
hung the run on the queue -- 2.4 hardens the worker). Tests: the store-only
policy, the Model-bound policy, the two refusals, the receiver exception, and
every committed template under plane/tasks/ rendering exactly the store plus,
for verify and roles, the provider host.

Gates: pytest 2027 passed, 42 skipped, exit 0; ruff check 0; ruff format 0;
mypy 0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-10-02 21:29:32 +02:00
co-authored by Claude Fable 5.1
parent 9506a18b56
commit c49eae4f30
4 changed files with 76 additions and 24 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ go-ahead** with a ceiling; steps that need a hand on a cluster that does not exi
- `tests/test_plane_runner.py`: accepted body with `delivery`; 400 on an `http` or IP URL; PUT to a local
server; inputs fetched from presigned-shaped URLs; the marker and retry-only boot unchanged.
- _Requirements: 3.1_
- [ ] 2.3 Egress without the receiver
- [x] 2.3 Egress without the receiver
- `src/openultrasast/plane/egress.py`: `policy_for(task, workspaces, model, store_host)` emits one
`tlsPassthrough` rule on 443 over the sorted union of store host, Git hosts, Model hosts; no `http` rule; an
IP store host raises naming `S3_ENDPOINT`.
+24 -9
View File
@@ -84,17 +84,32 @@ def git_hosts(workspaces: Iterable[Workspace]) -> tuple[list[str], list[str]]:
return sorted(tls), sorted(plain)
def policy_for(task: Task, workspaces: Iterable[Workspace], model: Model | None, receiver_host: str | None) -> dict[str, Any]:
"""The task's EgressPolicy body: the receiver over HTTP/80, Git and Model hosts as TLS passthrough on 443."""
def policy_for(
task: Task,
workspaces: Iterable[Workspace],
model: Model | None,
store_host: str | None,
receiver_host: str | None = None,
) -> dict[str, Any]:
"""The task's EgressPolicy body (plane-on-kubernetes design section 3): one ``tlsPassthrough`` rule on 443 over
the sorted union of the store's hostname (``store_host``: where the runner PUTs its tar and GETs its inputs), the
bound Workspaces' https Git hosts and the bound Model's ``openultrasast.io/egress-hosts``. No ``http`` rule, unless
``receiver_host`` names the old receiver (the path task 2.6 removes). An http Git repo cannot be reached under
this policy and is refused; a store given as an address is refused naming ``S3_ENDPOINT`` (the gateway allows
hostnames only)."""
tls, plain = git_hosts(workspaces)
tls = sorted(set(tls) | set(model.egress_hosts if model else ()))
if receiver_host and not _is_ip(receiver_host): # an address cannot be allowed; only the direct test mode has one
plain = sorted({*plain, receiver_host.lower()})
rules: list[dict[str, Any]] = []
if plain:
rules.append({"http": {"hostnames": plain, "ports": {"numbers": [80]}}})
if tls:
rules.append({"tlsPassthrough": {"hostnames": tls, "ports": {"numbers": [443]}}})
raise ValueError(f"Task/{task.metadata.name}: http Git repos ({', '.join(plain)}) cannot be reached: the policy grants TLS only")
hosts = set(tls) | set(model.egress_hosts if model else ())
if store_host:
if _is_ip(store_host):
raise ValueError(f"Task/{task.metadata.name}: the store endpoint is an address ({store_host}); S3_ENDPOINT must be a hostname")
hosts.add(store_host.lower())
rules: list[dict[str, Any]] = []
if receiver_host and not _is_ip(receiver_host): # the receiver path, kept for group 2 only (task 2.6 deletes it)
rules.append({"http": {"hostnames": [receiver_host.lower()], "ports": {"numbers": [80]}}})
if hosts:
rules.append({"tlsPassthrough": {"hostnames": sorted(hosts), "ports": {"numbers": [443]}}})
return {"rules": rules}
+1 -1
View File
@@ -341,7 +341,7 @@ def _execute(
docs = render_task(run, entry, manifests, receiver.url, receiver.dial)
manifest.write_text(yaml.safe_dump_all(docs, sort_keys=False), encoding="utf-8")
model = manifests.models.get(task.metadata.annotations.get(MODEL_ANNOTATION) or "")
policy = policy_for(task, [manifests.workspaces[b.name] for b in task.workspaces], model, urlsplit(receiver.url).hostname)
policy = policy_for(task, [manifests.workspaces[b.name] for b in task.workspaces], model, None, urlsplit(receiver.url).hostname)
try: # Req 4.6: the bound Model's credential is read here and only ever sent in the start request
start = router.starter(atespace, ax_name, run.metadata.name, credentials(model))
ax.apply(manifest) # creates the Substrate actor; its egress is denied until the policy exists
+50 -13
View File
@@ -1,8 +1,10 @@
"""ai-service-plane task 0: the per-task egress policy, its kubectl-ate calls, and the runner's dialled delivery.
Agent Substrate's egress gateway denies by default; :func:`egress.policy_for` must grant exactly the receiver (HTTP
by name), the bound Workspaces' Git hosts and the bound Model's API host (TLS passthrough), and nothing a gateway
would reject or that would intercept TLS: no IP address, no wildcard, no ``https`` rule.
Agent Substrate's egress gateway denies by default; :func:`egress.policy_for` must grant exactly the memory store's
host (where the runner PUTs its tar and GETs its inputs), the bound Workspaces' Git hosts and the bound Model's API
host, all as one TLS passthrough rule on 443 (plane-on-kubernetes 2.3), and nothing a gateway would reject or that
would intercept TLS: no IP address, no wildcard, no ``https`` rule, no ``http`` rule (the receiver path, kept for
group 2 only, is the one exception).
"""
from __future__ import annotations
@@ -70,24 +72,59 @@ def assert_tight(policy: dict) -> None:
ipaddress.ip_address(host)
def test_model_free_task_gets_only_the_receiver(manifests) -> None:
policy = egress.policy_for(manifests.tasks["facts"], [], None, egress.RECEIVER_HOST)
assert policy == {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
STORE = "files.store.example"
def test_model_free_task_gets_only_the_store(manifests) -> None:
policy = egress.policy_for(manifests.tasks["facts"], [], None, STORE)
assert policy == {"rules": [{"tlsPassthrough": {"hostnames": [STORE], "ports": {"numbers": [443]}}}]}
assert_tight(policy)
assert egress.policy_for(manifests.tasks["facts"], [], None, None) == {"rules": []}, "no store, no Git, no Model: nothing"
def test_the_receiver_path_adds_the_one_http_rule_until_it_is_removed(manifests) -> None:
policy = egress.policy_for(manifests.tasks["facts"], [], None, None, receiver_host=egress.RECEIVER_HOST)
assert policy == {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
assert egress.policy_for(manifests.tasks["facts"], [], None, None, receiver_host="127.0.0.1") == {"rules": []}
def test_model_bound_task_with_git_workspaces(manifests) -> None:
task = manifests.tasks["verify"]
policy = egress.policy_for(task, [manifests.workspaces["cases"]], manifests.models["deepseek-flash"], egress.RECEIVER_HOST)
policy = egress.policy_for(task, [manifests.workspaces["cases"]], manifests.models["deepseek-flash"], STORE)
assert policy["rules"] == [
{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}},
{"tlsPassthrough": {"hostnames": ["api.deepseek.com", "github.com"], "ports": {"numbers": [443]}}},
], "one github.com for two repos; the ssh and IP-address repos add nothing"
{"tlsPassthrough": {"hostnames": ["api.deepseek.com", STORE, "github.com"], "ports": {"numbers": [443]}}},
], "one rule: the store, one github.com for two repos, the Model host; the ssh and IP-address repos add nothing"
assert_tight(policy)
def test_an_ip_receiver_is_not_granted(manifests) -> None:
assert egress.policy_for(manifests.tasks["facts"], [], None, "127.0.0.1") == {"rules": []}
def test_an_ip_store_endpoint_is_refused_naming_the_variable(manifests) -> None:
with pytest.raises(ValueError, match="S3_ENDPOINT must be a hostname"):
egress.policy_for(manifests.tasks["facts"], [], None, "127.0.0.1")
def test_an_http_git_repo_is_refused(tmp_path: Path) -> None:
path = tmp_path / "m.yaml"
path.write_text(MANIFESTS.replace("https://github.com/example-org/alpha", "http://mirror.example/alpha"), encoding="utf-8")
loaded = load_manifests([path])
with pytest.raises(ValueError, match="http Git repos .*mirror.example.* cannot be reached"):
egress.policy_for(loaded.tasks["verify"], [loaded.workspaces["cases"]], None, STORE)
def test_every_committed_template_renders_the_expected_policy() -> None:
"""For each template under plane/tasks/: the store, the Model's hosts when bound, nothing else; one TLS rule."""
from openultrasast.plane.generate import TEMPLATES
plane = Path("plane")
loaded = load_manifests([plane / "tasks" / f"{name}.yaml" for name in TEMPLATES] + sorted((plane / "models").glob("*.yaml")))
assert set(loaded.tasks) == set(TEMPLATES)
for name, task in loaded.tasks.items():
model = loaded.models.get(task.metadata.annotations.get("openultrasast.io/model") or "")
policy = egress.policy_for(task, [], model, STORE)
expected = sorted({STORE, *(model.egress_hosts if model else ())})
assert policy == {"rules": [{"tlsPassthrough": {"hostnames": expected, "ports": {"numbers": [443]}}}]}, name
assert_tight(policy)
bound = {n for n, t in loaded.tasks.items() if "openultrasast.io/model" in t.metadata.annotations}
assert bound == {"verify", "roles"}, "the model-bound templates, and only they, get the provider host"
def test_deepseek_model_manifest_names_the_endpoint_host() -> None:
@@ -121,7 +158,7 @@ def calls(tmp_path: Path) -> list[str]:
return [" ".join(line.split()[2:4]) for line in (tmp_path / "ax.log").read_text().splitlines()]
POLICY = {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
POLICY = {"rules": [{"tlsPassthrough": {"hostnames": ["files.store.example"], "ports": {"numbers": [443]}}}]}
def test_apply_creates_reads_back_and_delete_confirms_the_cascade(tmp_path: Path, kubectl_ate: Path) -> None: