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.
This commit is contained in:
tegwick 2026-08-05 17:23:03 +02:00
parent 6383944939
commit 94a15d67aa
7 changed files with 207 additions and 8 deletions

View file

@ -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."

View file

@ -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}"

View file

@ -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 <ns> get svc <name> -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<ns>[a-z0-9-]+)/(?P<svc>[a-z0-9-]+)(?::(?P<port>\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()