CI exposed a latent validator defect masked locally: the desktop runner's Gitea-registered name is 'rune-desktop' (label 'rune-ci-desktop'), but validate_placement required the LABEL string inside runner_name — so every CI-executed stage was PLACEMENT_REJECTED while local runs passed (local: prefix path). Fix: desktop identity = label in name/labels, OR the registered rune-desktop runner name, OR verified local execution. Pi rejection semantics unchanged (regression-tested: rune-pi/rune-infra-pi still rejected via name, hostname, and labels). Found via RP11 PR CI — exactly what RP12's post-cleanup re-run is designed to catch, caught one run early.
127 lines
5.0 KiB
Python
127 lines
5.0 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"}
|