From a387c0ed88ae0c7bbf0855df157f61370c56654a Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:04:27 -0400 Subject: [PATCH] =?UTF-8?q?fix(executor):=20real=20k8s=20dispatch=20?= =?UTF-8?q?=E2=80=94=20explicit-context=20safety=20+=20create-not-apply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardening + a real defect caught before it reached a cluster. Safety: the K8sAdapter no longer dispatches to whatever kube-context happens to be current (which, on this box, is prod GKE). Applying now REQUIRES an explicit SOURCEOS_KUBE_CONTEXT; without it, apply is refused (manifest still emitted). Target namespace via SOURCEOS_KUBE_NAMESPACE (default sourceos-mesh), stamped into the manifest. Defect fix: dispatch used `kubectl apply`, which rejects generateName ("cannot use generate name with apply"). A Job is one-shot + immutable, so `kubectl create` is correct — and it supports generateName (collision-free job names per dispatch). Verified the emitted manifest with `kubectl create --dry-run=client` (client-side, no cluster/server contact): "job.batch/ created (dry run)". Tests: +2 (manifest carries namespace; apply refused without explicit context) = 92 green. Full live-apply on an ephemeral kind cluster is one `docker`+`kind up` away; the mechanism is proven and the prod-dispatch footgun is closed. --- docs/EXECUTION_SPINE.md | 7 +++++-- tools/executor.py | 26 ++++++++++++++++++++------ tools/test_executor.py | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/EXECUTION_SPINE.md b/docs/EXECUTION_SPINE.md index 555d207..682e772 100644 --- a/docs/EXECUTION_SPINE.md +++ b/docs/EXECUTION_SPINE.md @@ -32,8 +32,11 @@ mesh_telemetry compute_plane mcp_a2a_grant mcp_a2a_gr attestation, effect. No valid Grant → **`DispatchRefused`**, nothing runs. 5. **Dispatch** — a per-backend adapter runs it: - **local** — a real subprocess (`--apply`). - - **k8s** — a real, Grant-labelled `batch/v1` Job manifest (applied via `kubectl` when a cluster - is reachable, else emitted). + - **k8s** — a real, Grant-labelled `batch/v1` Job manifest, `kubectl create`-d (not `apply` — a + Job is one-shot/immutable and `apply` rejects `generateName`) into an **explicit** target + context. Applying requires `SOURCEOS_KUBE_CONTEXT` — the executor refuses to dispatch to + whatever kube-context happens to be current (which could be prod). Validated client-side with + `kubectl create --dry-run=client`. - **hpc-slurm · wasm-edge · p2p-mesh · volunteer-boinc · blockchain-rlc** — the substrate-specific descriptor to hand that scheduler over a Grant-bound channel. 6. **Receipt** — every dispatch is hash-sealed. diff --git a/tools/executor.py b/tools/executor.py index 2eec5eb..90ab945 100644 --- a/tools/executor.py +++ b/tools/executor.py @@ -86,13 +86,27 @@ def manifest(self, workload, decision, grant): } def dispatch(self, workload, decision, grant, *, apply): + import os + context = os.environ.get("SOURCEOS_KUBE_CONTEXT") + namespace = os.environ.get("SOURCEOS_KUBE_NAMESPACE", "sourceos-mesh") manifest = self.manifest(workload, decision, grant) - if apply and shutil.which("kubectl"): - proc = subprocess.run(["kubectl", "apply", "-f", "-"], input=json.dumps(manifest), - capture_output=True, text=True, timeout=60) - return {"kind": "k8s", "applied": proc.returncode == 0, "manifest": manifest, - "kubectl": proc.stdout.strip() or proc.stderr.strip()} - return {"kind": "k8s", "applied": False, "manifest": manifest} + manifest["metadata"]["namespace"] = namespace + if not apply: + return {"kind": "k8s", "applied": False, "namespace": namespace, "manifest": manifest} + # Safety: NEVER dispatch to whatever kube-context happens to be current (that could be prod). + # Applying requires an explicit target context named in SOURCEOS_KUBE_CONTEXT. + if not context: + return {"kind": "k8s", "applied": False, "namespace": namespace, "manifest": manifest, + "reason": "refusing to apply without SOURCEOS_KUBE_CONTEXT — won't dispatch to the current context"} + if not shutil.which("kubectl"): + return {"kind": "k8s", "applied": False, "namespace": namespace, "manifest": manifest, + "reason": "kubectl not found"} + # `create`, not `apply`: a Job is one-shot + immutable, and `apply` rejects generateName. + cmd = ["kubectl", "--context", context, "create", "-n", namespace, "-f", "-"] + proc = subprocess.run(cmd, input=json.dumps(manifest), capture_output=True, text=True, timeout=60) + return {"kind": "k8s", "applied": proc.returncode == 0, "namespace": namespace, + "context": context, "manifest": manifest, + "kubectl": proc.stdout.strip() or proc.stderr.strip()} class DescriptorAdapter: diff --git a/tools/test_executor.py b/tools/test_executor.py index cd51df2..721ed6b 100644 --- a/tools/test_executor.py +++ b/tools/test_executor.py @@ -60,6 +60,27 @@ def test_k8s_adapter_emits_a_valid_grant_labelled_job(): assert m["spec"]["template"]["spec"]["restartPolicy"] == "Never" +def test_k8s_manifest_carries_the_target_namespace(): + d = _decision("k8s") + res = ex.execute({"command": "echo x", "effect": "exec"}, d, _grant(d, "exec"), + session_id="sess_exec1", verifier=VERIFIER, apply=False) + assert res["dispatch"]["manifest"]["metadata"]["namespace"] == "sourceos-mesh" + + +def test_k8s_apply_refuses_without_an_explicit_context(): + # applying must NOT fall back to the current kube-context (could be prod) — it needs an explicit one. + import os + saved = os.environ.pop("SOURCEOS_KUBE_CONTEXT", None) + try: + d = _decision("k8s") + res = ex.execute({"command": "echo x", "effect": "exec"}, d, _grant(d, "exec"), + session_id="sess_exec1", verifier=VERIFIER, apply=True) + assert res["dispatch"]["applied"] is False and "context" in res["dispatch"]["reason"] + finally: + if saved is not None: + os.environ["SOURCEOS_KUBE_CONTEXT"] = saved + + def test_descriptor_adapter_emits_a_backend_specific_descriptor(): d = _decision("hpc-slurm") res = ex.execute({"command": "srun train", "effect": "compute"}, d, _grant(d, "compute"),