Files
rune/scripts/runner_evidence.py
T
Nick ca4f7593d7
CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 14s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 34s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 48s
feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
CORRECTION 1: #314 reopened — recall_with_associations mode field defect
CONFIRMED in current source and FIXED (mode: association_merged).

CORRECTION 3: Gitea credential removed from remote URL (credential helper).
TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires
Gitea web UI access). Remote URL now shows username only, no secret.

CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence
computed warranted bump from commits-since-tag without checking if master
version already satisfies the bump. FIX: already-converged guard in
version_convergence.py (master version > last tag → no commit). Repo-aware
(args.repo passed to read_version/latest_tag).

CORRECTION 6: lifecycle terminalization — execution exception now
guarantees task terminalizes as failed with evidence (no immortal RUNNING).

CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction
(NOT shared operator state). Read-only leading-verb override. Isolation
tests prove no cross-contamination between interleaved tasks.

PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure
class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics,
route-instruction learning loop (PE-1789102658-0269 SEED).

1253 tests green; ratchet 352/80 (shrink); henxels clean.
2026-09-11 15:36:08 +01:00

132 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""runner_evidence.py — runtime execution placement evidence (RP2/#265).
Canonical rule: THE HEAVY RUNNER IS THE DESKTOP PC, NOT THE PI.
A desktop-heavy stage executed on the Pi must NOT count as valid acceptance.
This module captures ACTUAL runtime evidence — not configuration inference:
- hostname of the executing machine
- runner identity (from CI env vars when present, else "local")
- runner labels/capabilities
- execution class (desktop-heavy / pi-lightweight)
- stage, process id, issue, research process id, timestamp
The evidence record travels with the stage result and is validated by the
placement gate BEFORE a stage result can be accepted.
"""
from __future__ import annotations
import os
import socket
import time
from datetime import datetime, timezone
# Hostname substrings that identify the Pi fleet. Conservative: match on
# the runner name labels actually registered in Gitea, plus obvious names.
PI_IDENTIFIERS = ("rune-pi", "rune-infra-pi", "raspberry", "-pi")
# Desktop runner identity (the only valid executor for desktop-heavy stages).
DESKTOP_RUNNER_LABEL = "rune-ci-desktop"
# Stages that require desktop-heavy execution (cognition, research, coding).
DESKTOP_HEAVY_STAGES = {
"INWARD",
"OUTWARD",
"APPLICABILITY",
"INNOVATION",
"ADVERSARIAL",
"SYNTHESIS",
"QUALIFY",
}
# Lightweight forge/control work that MAY legitimately run on the Pi.
PI_LIGHTWEIGHT_OPERATIONS = {
"issue_metadata",
"label_change",
"artifact_metadata",
"process_state",
"eligibility_check",
"state_transition",
}
def _is_pi(name: str) -> bool:
lowered = (name or "").lower()
return any(marker in lowered for marker in PI_IDENTIFIERS)
def capture_execution_evidence(
stage: str,
issue: int | None = None,
process_id: str | None = None,
execution_class: str = "desktop-heavy",
) -> dict:
"""Capture WHERE this stage actually ran, from runtime facts.
Never trust configuration alone — capture the live hostname and any
runner identity present in the environment.
"""
hostname = socket.gethostname()
runner_name = os.environ.get("RUNNER_NAME") or os.environ.get("GITEA_RUNNER_NAME") or ("local:" + hostname)
runner_labels = [l.strip() for l in os.environ.get("RUNNER_LABELS", "").split(",") if l.strip()] or [runner_name]
return {
"runner_hostname": hostname,
"runner_name": runner_name,
"runner_labels": runner_labels,
"execution_class": execution_class,
"stage": stage,
"process_id": process_id or f"pid:{os.getpid()}",
"issue_id": issue,
"research_process_id": process_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"monotonic": time.monotonic(),
}
def validate_placement(evidence: dict) -> tuple[bool, list[str]]:
"""Deterministic placement gate (henxel).
Returns (accepted, violations). A desktop-heavy stage executed on the
Pi is REJECTED — no silent fallback, no configuration-only inference.
"""
violations = []
stage = evidence.get("stage", "")
klass = evidence.get("execution_class", "desktop-heavy")
# Evidence completeness — identity must be actually captured
for field in ("runner_hostname", "runner_name", "runner_labels", "execution_class", "stage", "timestamp"):
if not evidence.get(field):
violations.append(f"placement evidence missing field: {field}")
if klass == "desktop-heavy":
identity = evidence.get("runner_name", "") + " " + evidence.get("runner_hostname", "")
if _is_pi(identity) or any(_is_pi(l) for l in evidence.get("runner_labels", [])):
violations.append(
f"PLACEMENT VIOLATION: desktop-heavy stage '{stage}' executed on Pi "
f"(runner={evidence.get('runner_name')}, host={evidence.get('runner_hostname')}). "
f"Rejected as valid acceptance — the heavy runner is the desktop PC, not the Pi."
)
# Desktop identity = Gitea runner label (rune-ci-desktop), the Gitea
# runner NAME (rune-desktop), or a verified local desktop execution.
# Pi rejection above already caught Pi markers — this is the allow-side.
labels = " ".join(evidence.get("runner_labels", []))
if (
DESKTOP_RUNNER_LABEL not in identity
and "rune-desktop" not in identity
and DESKTOP_RUNNER_LABEL not in labels
and "local:" not in evidence.get("runner_name", "")
):
violations.append(
f"runner '{evidence.get('runner_name')}' is not '{DESKTOP_RUNNER_LABEL}' "
f"nor the registered rune-desktop runner, and not a verified local "
f"desktop execution"
)
return (len(violations) == 0, violations)
def summarize_for_artifact(evidence: dict) -> dict:
"""Strip transient fields for embedding in a stage artifact."""
return {k: v for k, v in evidence.items() if k != "monotonic"}