#!/usr/bin/env python3 """Claim-loop launcher: load claim-loop.env and resolve k8s:// Service URLs. Used as ~/bin/rein-aharness-claim on railiance01 so shell never sources JSON values (AGENT_HARNESS_REPO_MAP). Supports durable ClusterIP access without kubectl port-forward (ACTIVITY-WP-0027 / docs in activity-core). Env file keys may use: LLM_CONNECT_URL=k8s://activity-core/llm-connect:8080 ACTIVITY_CORE_URL=k8s://activity-core/actcore-api:8010 STATE_HUB_URL=k8s://activity-core/actcore-statehub-edge-relay:8000 Resolution uses: kubectl -n get svc -o jsonpath={.spec.clusterIP} """ from __future__ import annotations import os import re import subprocess import sys from pathlib import Path _K8S_URL = re.compile( r"^k8s://(?P[a-z0-9-]+)/(?P[a-z0-9-]+)(?::(?P\d+))?/?$" ) def _load_env_file(path: Path) -> dict[str, str]: out: dict[str, str] = {} if not path.is_file(): return out for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) out[key.strip()] = value.strip() return out def _resolve_k8s_url(value: str) -> str: m = _K8S_URL.match(value.strip()) if not m: return value ns = m.group("ns") svc = m.group("svc") port = m.group("port") or "80" kubeconfig = os.environ.get("KUBECONFIG", "/etc/rancher/k3s/k3s.yaml") cmd = [ "kubectl", f"--kubeconfig={kubeconfig}", "-n", ns, "get", "svc", svc, "-o", "jsonpath={.spec.clusterIP}", ] try: ip = subprocess.check_output(cmd, text=True, timeout=30).strip() except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as exc: print( f"rein-aharness-claim: failed to resolve {value!r}: {exc}", file=sys.stderr, ) raise SystemExit(2) from exc if not ip or ip == "None": print( f"rein-aharness-claim: empty ClusterIP for {ns}/{svc}", file=sys.stderr, ) raise SystemExit(2) return f"http://{ip}:{port}" def _resolve_env(env: dict[str, str]) -> dict[str, str]: keys = ( "LLM_CONNECT_URL", "ACTIVITY_CORE_URL", "STATE_HUB_URL", "STATEHUB_URL", ) for key in keys: if key in env and env[key].startswith("k8s://"): env[key] = _resolve_k8s_url(env[key]) return env def main() -> None: home = Path.home() env = os.environ.copy() # Default kubeconfig for k3s host users env.setdefault("KUBECONFIG", "/etc/rancher/k3s/k3s.yaml") file_env = _load_env_file(home / ".config/rein-aharness/claim-loop.env") env.update(file_env) env = _resolve_env(env) rein = home / "rein-aharness" / ".venv" / "bin" / "rein-aharness" if not rein.is_file(): # Fall back to PATH cmd = ["rein-aharness", *sys.argv[1:]] else: cmd = [str(rein), *sys.argv[1:]] raise SystemExit(subprocess.call(cmd, env=env)) if __name__ == "__main__": main()