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
+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