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()

View file

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

View file

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

View file

@ -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,

View file

@ -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: