rein-aharness/docs/ops-run-claim-loop.md
tegwick 353554eb49
Some checks failed
Governed runtime contract / contract (push) Failing after 18s
Prove installed Railiance runtime recovery under worker failures
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e387-534d-70e3-ad53-4ea05676db8c
2026-09-27 18:00:35 +02:00

11 KiB

Ops run claim loop (REIN-A-0002)

Primary intake for activity-core scheduled automation.
Architecture: ACT-ADR-005 · activity-core ops-run-queue.md

Does not require issue-core or Forgejo for FI / Binky-style ops.

Flow

activity-core emit_tasks
  → INSERT ops_run (open, optional profile + repository_grant) + task_spawn
        │
        ▼
rein-aharness claim-loop
  POST /ops-runs/claim
  POST /ops-runs/{id}/heartbeat (immediate ownership proof; fail closed)
  harness_profile_ref present? ─ yes → Glas ExecutionRequest → gateway
        │ no
        └─→ select_approach(labels, definition, hint) → legacy executor
  validate granted repository result + persist external metrics
  enqueue exact terminal close → POST complete | fail
  replay pending close evidence before another claim

harness_profile_ref is authoritative under ACT-ADR-006. A profiled row never consults approach_hint, select_approach, or a legacy executor, including after profile resolution refusal. The claim worker sends bounded ExecutionEvidence; direct tool output/error and unknown provider fields are not persisted in its close outbox. A separately typed grant activates commit, path, clean-state, metadata, and remote-ref acceptance plus external metrics. Profile-absent rows keep the legacy coexistence path, but cannot carry a grant.

Environment

Variable Default Meaning
ACTIVITY_CORE_URL http://127.0.0.1:8010 actcore-api base
ACTIVITY_CORE_WORKER_TOKEN unset X-Worker-Token / Bearer
AGENT_HARNESS_WORKER_ID rein-aharness@hostname claim owner
AGENT_HARNESS_OPS_LABELS automated claim label filter
AGENT_HARNESS_OPS_LABELS_MODE any any or all
AGENT_HARNESS_OPS_LEASE_SECONDS 900 claim lease
AGENT_HARNESS_CLAIM_INTERVAL 30 empty-queue poll interval
AGENT_HARNESS_REQUIRED_PROFILE_REFS unset comma-separated exact production profile@version refs checked at startup
AGENT_HARNESS_LEGACY_APPROACHES_UNTIL unset ISO expiry date required for profile-absent compatibility routing
AGENT_HARNESS_REPO_MAP {} JSON slug→checkout path
AGENT_HARNESS_REPO_ROOTS ~:~/work slug search roots
REIN_AHARNESS_STATE_DIR user state directory private repository-lock, metrics, and close-outbox state

Example map on railiance01:

export AGENT_HARNESS_REPO_MAP='{
  "freedom-intelligence":"/home/tegwick/freedom-intelligence",
  "binky-control":"/home/tegwick/binky-control"
}'
export AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
export ACTIVITY_CORE_URL=http://127.0.0.1:8010   # or ClusterIP via tunnel
export ACTIVITY_CORE_WORKER_TOKEN=…               # from actcore-runtime-secret

Approach registry

Match Approach
labels research-brief / freedom-intelligence or fi-daily* fi-research-brief
labels rhythm / binky-daily or binky-daily* brief-daily
labels binky + weekly-review or binky-weekly-review* brief-weekly
labels mail-intake mail-scan then mail-triage
labels agent-session agentic run (TaskSpec)
none fail ops_run (no silent drop)

For a profile-absent legacy row, optional approach_hint overrides matching. It has no effect when harness_profile_ref is present.

Add a row: edit rein_aharness/approaches.py (APPROACH_RULES + execute_approach) and tests in tests/test_approaches.py. Prefer cheapest correct path (structured llm-connect before open-ended agent session).

CLI

# Peek open ops_runs + selected approach
rein-aharness poll --source=ops-run --no-claim

# Claim one, execute approach, complete/fail
rein-aharness run --from-ops-run
# or
rein-aharness poll --source=ops-run

# Continuous worker (systemd)
rein-aharness claim-loop
rein-aharness claim-loop --once --verbose

# Read-only startup readiness (never claims)
rein-aharness preflight

# Required close evidence and recovery
rein-aharness close-outbox status
rein-aharness close-outbox replay

# Dry-run: claim then fail+reopen without domain work
rein-aharness poll --source=ops-run --dry-run

Legacy issue-core (external tickets only):

rein-aharness poll --source=issue-core
rein-aharness run --from-issue-core

Install claim-loop on railiance01 (user systemd)

# From rein-aharness checkout on railiance01
./deploy/scripts/install-pinned-runtime.sh
./deploy/scripts/install-claim-loop-user.sh

# Or manually:
mkdir -p ~/.config/systemd/user
cp deploy/systemd/rein-aharness-claim-loop.service ~/.config/systemd/user/
# Edit Environment= paths / token in the unit or env file
systemctl --user daemon-reload
systemctl --user enable --now rein-aharness-claim-loop.service
journalctl --user -u rein-aharness-claim-loop -f

The unit runs rein-aharness preflight through the same environment-loading wrapper before every start. It probes Activity Core with a read-only open-run list, verifies configured repository workspaces and private runtime state, and refuses unresolved close-evidence quarantine without claiming or executing work. Pending close evidence is allowed through startup because the first claim-loop cycle replays it before any new claim; quarantine requires operator review.

Set AGENT_HARNESS_REQUIRED_PROFILE_REFS to the comma-separated exact profile@version references enabled for production. Every named profile must resolve through the installed Glas catalog with operational_readiness: ready. A ready profile.bwrap-local also executes a minimal bwrap namespace probe, which detects missing bubblewrap and host user-namespace/AppArmor refusal. Leaving the variable empty declares that no profiled production definition is enabled; this does not make a blocked profile executable or introduce fallback.

Use rein-aharness preflight --offline only while installing or diagnosing local files. It skips the Activity Core probe and is not the systemd gate.

install-pinned-runtime.sh first verifies clean sibling checkouts against deploy/runtime-contract-lock.json, then applies the checked-in uv.lock with --frozen --no-editable. This prevents the authoritative service from running against whichever mutable editable sibling happened to be present. Updating a runtime contract requires reviewing and committing both locks together.

make contract-test is the non-skippable cross-package release gate. It imports Glas, sand-boxer, and llm-connect before running the contract/claim suites, so the optional pytest.importorskip development behavior cannot turn a missing production dependency into a green release result.

Forgejo runs the same gate from .forgejo/workflows/runtime-contract.yaml on every main push. It follows the estate runner contract (direct clone, no actions/checkout), detaches every sibling at the revision declared in the runtime lock, applies the frozen uv resolution, and needs no deployment or cluster credential.

make recovery-test is the deterministic pre-deploy recovery gate. It covers initial and periodic lease rejection, adapter cancellation, repository-lock release, response-lost close reconciliation, pending-close claim blocking, every permanent Activity Core close code (including expired_lease), and Glas cancellation. It does not replace the isolated expired-row API smoke or the natural-run and sandbox-cleanup observations required from the deployed host.

Installed-runtime recovery drill

Run scripts/prove-runtime-recovery.py with the selected artifact's interpreter:

"${RUNTIME}/bin/python3" -I -B scripts/prove-runtime-recovery.py \
  --runtime "${RUNTIME}" --sha256 "${RUNTIME_SHA256}" \
  --profile-ref harness.agent-dev-local@1.1.1

The script verifies the complete artifact digest and that all four runtime packages load from that artifact. It uses private temporary repositories, sandbox stores and worker state. The queue and rein authoring are fixtures; the installed worker, Glas lifecycle, bwrap execution, repository acceptance, external metrics and close outbox are real. No provider or production queue client is invoked. The selected profile is made ready only in an in-memory fixture catalog; no installed profile or standing service is changed.

The receipt covers response-lost close replay with one execution/commit, execution failure, periodic lease rejection, a real SIGTERM to the disposable worker, a killed lock holder, and SIGKILL of a disposable worker after sandbox creation. Cancellation deliberately returns a late sandbox commit to prove that it is not imported into the source. Every case checks lock reacquisition, source state, sandbox/workspace cleanup and applicable durable evidence.

After abrupt worker death, cleanup is explicit owner recovery, not an automatic startup sweep. Recover only the sandbox id attributed to the failed run, using sand-boxer's get/destroy with the same owner state directory. Verify destruction and workspace removal before resuming. Do not delete lock files to release a kernel lock, erase pending close evidence, or replay the workload to reconcile a close. Use close-outbox status|replay for that last step. Activity Core remains responsible for expiring the abandoned queue lease.

Railiance receipt: docs/evidence/2026-09-27-installed-runtime-recovery.json. It proves these host mechanisms against runtime b6e4e8a4; it does not prove live Activity Core expiry, credential delivery, or a paid production run.

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.

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

kubectl -n activity-core port-forward svc/actcore-api 8010:8010
export ACTIVITY_CORE_URL=http://127.0.0.1:8010

Cutover from host timers (T05)

Host oneshot timers (FI 07:35, Binky rhythm) remain break-glass until:

  1. claim-loop is running and healthy
  2. 5 clean weekday cycles (or 5 forced triggers) with open→claimed→succeeded
  3. Then disable or demote timers — see deploy/docs/timer-cutover.md

Until then dual-path is intentional: schedule truth is activity-core; timers are a safety net if claim-loop is down.

Labels required on definitions

Domain Labels (min)
Freedom Intelligence automated, research-brief (already on FI rule)
Binky daily automated, rhythm
Binky mail automated, mail-intake