From 94a15d67aa606628578e96a648a6e25da5a58ea8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 5 Aug 2026 17:23:03 +0200 Subject: [PATCH] feat: k8s:// ClusterIP claim env + complete payload target_repo Resolve llm-connect / actcore-api / edge-relay without port-forward. Include target_repo on FI and Binky complete payloads for ops UI artefacts. --- deploy/scripts/install-claim-loop-user.sh | 11 +- deploy/scripts/refresh-claim-loop-k8s-urls.sh | 59 ++++++++++ deploy/scripts/rein-aharness-claim | 110 ++++++++++++++++++ deploy/systemd/claim-loop.env.example | 10 +- docs/ops-run-claim-loop.md | 19 ++- rein_aharness/approaches.py | 2 + rein_aharness/claim_loop.py | 4 + 7 files changed, 207 insertions(+), 8 deletions(-) create mode 100755 deploy/scripts/refresh-claim-loop-k8s-urls.sh create mode 100755 deploy/scripts/rein-aharness-claim diff --git a/deploy/scripts/install-claim-loop-user.sh b/deploy/scripts/install-claim-loop-user.sh index 0ddde6f..21e2fb4 100755 --- a/deploy/scripts/install-claim-loop-user.sh +++ b/deploy/scripts/install-claim-loop-user.sh @@ -6,16 +6,20 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" UNIT_DIR="${HOME}/.config/systemd/user" ENV_DIR="${HOME}/.config/rein-aharness" -mkdir -p "${UNIT_DIR}" "${ENV_DIR}" +BIN_DIR="${HOME}/bin" +mkdir -p "${UNIT_DIR}" "${ENV_DIR}" "${BIN_DIR}" cp -f "${ROOT}/deploy/systemd/rein-aharness-claim-loop.service" "${UNIT_DIR}/" +# Durable env loader + k8s:// ClusterIP resolution (no port-forward) +install -m 755 "${ROOT}/deploy/scripts/rein-aharness-claim" "${BIN_DIR}/rein-aharness-claim" if [[ ! -f "${ENV_DIR}/claim-loop.env" ]]; then cp "${ROOT}/deploy/systemd/claim-loop.env.example" "${ENV_DIR}/claim-loop.env" chmod 600 "${ENV_DIR}/claim-loop.env" - echo "Created ${ENV_DIR}/claim-loop.env — set ACTIVITY_CORE_URL, token, REPO_MAP." + echo "Created ${ENV_DIR}/claim-loop.env — set ACTIVITY_CORE_WORKER_TOKEN and REPO_MAP paths." else echo "Keeping existing ${ENV_DIR}/claim-loop.env" + echo " Tip: migrate URLs with ${ROOT}/deploy/scripts/refresh-claim-loop-k8s-urls.sh" fi # Ensure package entrypoint exists @@ -28,4 +32,5 @@ systemctl --user enable rein-aharness-claim-loop.service echo "Enabled. Start when env is filled:" echo " systemctl --user start rein-aharness-claim-loop.service" echo " journalctl --user -u rein-aharness-claim-loop -f" -echo "Host timers remain break-glass until T05 cutover (docs/ops-run-claim-loop.md)." +echo "llm-connect / actcore-api: use k8s:// URLs (see activity-core docs/llm-connect-host-access.md)." +echo "Do not rely on kubectl port-forward for production claim-loop." diff --git a/deploy/scripts/refresh-claim-loop-k8s-urls.sh b/deploy/scripts/refresh-claim-loop-k8s-urls.sh new file mode 100755 index 0000000..ad351ff --- /dev/null +++ b/deploy/scripts/refresh-claim-loop-k8s-urls.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Rewrite claim-loop.env URL keys to live k8s:// or concrete ClusterIP forms. +# Usage: +# ./deploy/scripts/refresh-claim-loop-k8s-urls.sh # write k8s:// pseudo-URLs +# ./deploy/scripts/refresh-claim-loop-k8s-urls.sh --concrete # write http://ClusterIP:port +set -euo pipefail + +ENV_FILE="${HOME}/.config/rein-aharness/claim-loop.env" +KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}" +MODE=k8s +if [[ "${1:-}" == "--concrete" ]]; then + MODE=concrete +fi + +if [[ ! -f "${ENV_FILE}" ]]; then + echo "missing ${ENV_FILE}" >&2 + exit 1 +fi + +resolve_ip() { + local ns="$1" svc="$2" + kubectl --kubeconfig="${KUBECONFIG}" -n "${ns}" get svc "${svc}" \ + -o jsonpath='{.spec.clusterIP}' +} + +url_for() { + local ns="$1" svc="$2" port="$3" + if [[ "${MODE}" == "k8s" ]]; then + echo "k8s://${ns}/${svc}:${port}" + else + local ip + ip="$(resolve_ip "${ns}" "${svc}")" + echo "http://${ip}:${port}" + fi +} + +ACTIVITY_CORE_URL="$(url_for activity-core actcore-api 8010)" +LLM_CONNECT_URL="$(url_for activity-core llm-connect 8080)" +STATE_HUB_URL="$(url_for activity-core actcore-statehub-edge-relay 8000)" + +tmp="$(mktemp)" +# shellcheck disable=SC2016 +awk -v ac="${ACTIVITY_CORE_URL}" -v llm="${LLM_CONNECT_URL}" -v sh="${STATE_HUB_URL}" ' + BEGIN { seen_ac=0; seen_llm=0; seen_sh=0 } + /^ACTIVITY_CORE_URL=/ { print "ACTIVITY_CORE_URL=" ac; seen_ac=1; next } + /^LLM_CONNECT_URL=/ { print "LLM_CONNECT_URL=" llm; seen_llm=1; next } + /^STATE_HUB_URL=/ { print "STATE_HUB_URL=" sh; seen_sh=1; next } + { print } + END { + if (!seen_ac) print "ACTIVITY_CORE_URL=" ac + if (!seen_llm) print "LLM_CONNECT_URL=" llm + if (!seen_sh) print "STATE_HUB_URL=" sh + } +' "${ENV_FILE}" > "${tmp}" +chmod 600 "${tmp}" +mv "${tmp}" "${ENV_FILE}" + +echo "Updated ${ENV_FILE} (${MODE}):" +grep -E '^(ACTIVITY_CORE_URL|LLM_CONNECT_URL|STATE_HUB_URL)=' "${ENV_FILE}" diff --git a/deploy/scripts/rein-aharness-claim b/deploy/scripts/rein-aharness-claim new file mode 100755 index 0000000..f5471d8 --- /dev/null +++ b/deploy/scripts/rein-aharness-claim @@ -0,0 +1,110 @@ +#!/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() diff --git a/deploy/systemd/claim-loop.env.example b/deploy/systemd/claim-loop.env.example index b42c80e..431eefa 100644 --- a/deploy/systemd/claim-loop.env.example +++ b/deploy/systemd/claim-loop.env.example @@ -1,6 +1,8 @@ # Copy to ~/.config/rein-aharness/claim-loop.env and chmod 600 -ACTIVITY_CORE_URL=http://127.0.0.1:8010 +# Prefer k8s:// pseudo-URLs — rein-aharness-claim resolves ClusterIP at start +# (no kubectl port-forward). See activity-core docs/llm-connect-host-access.md +ACTIVITY_CORE_URL=k8s://activity-core/actcore-api:8010 ACTIVITY_CORE_WORKER_TOKEN= AGENT_HARNESS_WORKER_ID=rein-aharness@railiance01 AGENT_HARNESS_OPS_LABELS=automated @@ -9,6 +11,6 @@ AGENT_HARNESS_OPS_LEASE_SECONDS=900 AGENT_HARNESS_CLAIM_INTERVAL=30 AGENT_HARNESS_REPO_MAP={"freedom-intelligence":"/home/tegwick/freedom-intelligence","binky-control":"/home/tegwick/binky-control"} AGENT_HARNESS_REPO_ROOTS=/home/tegwick:/home/tegwick/work -# LLM / hub as required by approaches -# LLM_CONNECT_URL=http://... -# STATE_HUB_URL=http://... +LLM_CONNECT_URL=k8s://activity-core/llm-connect:8080 +STATE_HUB_URL=k8s://activity-core/actcore-statehub-edge-relay:8000 +KUBECONFIG=/etc/rancher/k3s/k3s.yaml diff --git a/docs/ops-run-claim-loop.md b/docs/ops-run-claim-loop.md index b7fa617..212d0b6 100644 --- a/docs/ops-run-claim-loop.md +++ b/docs/ops-run-claim-loop.md @@ -101,7 +101,24 @@ systemctl --user enable --now rein-aharness-claim-loop.service journalctl --user -u rein-aharness-claim-loop -f ``` -Port-forward if claim-loop runs on host and API is ClusterIP-only: +### Host access to cluster services (no port-forward) + +On railiance01 (single-node k3s), set **k8s://** pseudo-URLs in +`claim-loop.env` (see `deploy/systemd/claim-loop.env.example`). The +`~/bin/rein-aharness-claim` wrapper resolves them to Service ClusterIPs at +start. Canon: activity-core `docs/llm-connect-host-access.md`. + +```bash +# migrate existing env +./deploy/scripts/refresh-claim-loop-k8s-urls.sh +# reinstall wrapper +./deploy/scripts/install-claim-loop-user.sh +systemctl --user restart rein-aharness-claim-loop.service +``` + +**Deprecated:** long-lived `kubectl port-forward` to 127.0.0.1:8010 / :8080. + +Legacy note (break-glass only) if ClusterIP routing is broken: ```bash kubectl -n activity-core port-forward svc/actcore-api 8010:8010 diff --git a/rein_aharness/approaches.py b/rein_aharness/approaches.py index 7de3757..6123534 100644 --- a/rein_aharness/approaches.py +++ b/rein_aharness/approaches.py @@ -227,6 +227,7 @@ def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResul "skipped_existing": r.skipped_existing, "collection_candidates": r.collection_candidates, "head_after": r.head_after, + "target_repo": "freedom-intelligence", }, reason=r.reason, reopen=not r.ok and not r.skipped_existing, @@ -253,6 +254,7 @@ def _run_brief_daily( "committed": r.committed, "skipped_existing": r.skipped_existing, "head_after": r.head_after, + "target_repo": "binky-control", }, reason=r.reason, reopen=not r.ok and not r.skipped_existing, diff --git a/rein_aharness/claim_loop.py b/rein_aharness/claim_loop.py index 9947800..ae3124e 100644 --- a/rein_aharness/claim_loop.py +++ b/rein_aharness/claim_loop.py @@ -157,8 +157,12 @@ def process_one( "approach": ar.approach, "ok": ar.ok, "reason": ar.reason, + "target_repo": run.target_repo, **(ar.result or {}), } + # Prefer approach-provided target_repo if set + if ar.result and ar.result.get("target_repo"): + payload["target_repo"] = ar.result["target_repo"] try: if ar.ok: