assembly-checkpoint: pin web-fetcher to v0.2.0 and pass a per-run --cache dir

The tenant runner (assembly-economics 2521955) refuses any web-fetcher other
than 0.2.0. Instead of defaulting to a moving ~/web-fetcher checkout, the
approach now uses an explicit WEB_FETCHER_PATH/ASSEMBLY_WEB_FETCHER_PATH or
keeps a shallow clone of tag v0.2.0 under ~/.cache/rein-aharness/ and refuses
it if HEAD is not exactly at the tag. Each run passes --cache to a fresh temp
directory outside the checkout; its path is reported in the hub detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 193986@bnt-lap001
Assistant-Session: 585bfbea-320e-47b3-8c0e-bc1513a53307
This commit is contained in:
tegwick 2026-10-19 14:58:39 +02:00
parent b80096133f
commit dcf444f001
3 changed files with 81 additions and 7 deletions

View file

@ -138,7 +138,7 @@ the same "no exact selector" gate as the four definitions above.
| Field | Current behavior |
|---|---|
| Owner | assembly-economics (coulomb); declaration `checkpoints/EXECUTION_DECLARATION.md`, founder-approved 2026-10-13 |
| Executor | `assembly_checkpoint.run_assembly_checkpoint` runs the tenant's own `checkpoints/run_checkpoint.py` (data via web-fetcher, optional llm-connect assessment) |
| Executor | `assembly_checkpoint.run_assembly_checkpoint` runs the tenant's own `checkpoints/run_checkpoint.py` (data via web-fetcher pinned to tag `v0.2.0`, cloned under `~/.cache/rein-aharness/` unless `WEB_FETCHER_PATH` is set; per-run `--cache` scratch dir outside the checkout; optional llm-connect assessment) |
| Schedule | Activity Core cron fires; `checkpoints/schedule.md` is authoritative, and out-of-window fires are successful no-ops |
| Repository mutation | Commits only under `checkpoints/` |
| Publication | Pushes `HEAD` fast-forward to origin, rebases once, never forces. The `checkpoints/` path grant is enforced only inside rein-aharness code (`PUBLISH_PREFIX`), not by a separately granted capability |

View file

@ -20,6 +20,7 @@ import os
import re
import subprocess
import sys
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@ -29,6 +30,9 @@ from rein_aharness import hub
PUBLISH_PREFIX = "checkpoints/"
RUNNER = "checkpoints/run_checkpoint.py"
EXIT_OUT_OF_WINDOW = 3
# The tenant runner refuses any other web-fetcher version (assembly-economics 2521955).
WEB_FETCHER_TAG = "v0.2.0"
WEB_FETCHER_URL = "https://forgejo.coulomb.social/coulomb/web-fetcher"
class AssemblyCheckpointError(RuntimeError):
@ -47,6 +51,7 @@ class AssemblyCheckpointResult:
origin_sha: str = ""
head_after: str = ""
counts: dict[str, Any] = field(default_factory=dict)
cache_dir: str = ""
reason: str | None = None
@ -74,6 +79,33 @@ def _sync_from_origin(repo: Path) -> None:
_git(repo, "merge", "--ff-only", f"origin/{branch}", timeout=60)
def ensure_web_fetcher(cache_root: Path | None = None) -> Path:
"""Path to a web-fetcher checkout of WEB_FETCHER_TAG, outside any target checkout.
An explicit WEB_FETCHER_PATH / ASSEMBLY_WEB_FETCHER_PATH wins (the runner still
refuses a wrong version). Otherwise a shallow clone of the tag is kept under the
cache root and re-verified on every run; a checkout that is not exactly at the
tag is refused rather than silently used.
"""
explicit = os.environ.get("WEB_FETCHER_PATH") or os.environ.get("ASSEMBLY_WEB_FETCHER_PATH")
if explicit:
return Path(explicit).expanduser()
root = cache_root or Path(os.environ.get("XDG_CACHE_HOME", "~/.cache")).expanduser() / "rein-aharness"
dest = root / f"web-fetcher-{WEB_FETCHER_TAG}"
url = os.environ.get("ASSEMBLY_WEB_FETCHER_URL", WEB_FETCHER_URL)
if not dest.exists():
dest.parent.mkdir(parents=True, exist_ok=True)
r = subprocess.run(["git", "clone", "--quiet", "--depth", "1", "--branch", WEB_FETCHER_TAG, url, str(dest)],
capture_output=True, text=True, timeout=300)
if r.returncode != 0:
raise AssemblyCheckpointError(f"web-fetcher {WEB_FETCHER_TAG} clone failed: {r.stderr.strip()[:200]}")
tag = subprocess.run(["git", "-C", str(dest), "describe", "--tags", "--exact-match", "HEAD"],
capture_output=True, text=True, timeout=60).stdout.strip()
if tag != WEB_FETCHER_TAG:
raise AssemblyCheckpointError(f"web-fetcher checkout {dest} is at {tag or 'no tag'}, not {WEB_FETCHER_TAG}")
return dest
def _publish(repo: Path) -> str:
"""Push HEAD; on rejection rebase checkpoint-only commits once and retry. Never force."""
branch = _branch(repo)
@ -102,10 +134,11 @@ def run_assembly_checkpoint(target_repo: Path, *, kind: str = "light", report_to
if sync and commit:
_sync_from_origin(repo)
env = dict(os.environ)
env.setdefault("WEB_FETCHER_PATH", str(Path(os.environ.get("ASSEMBLY_WEB_FETCHER_PATH",
"~/web-fetcher")).expanduser()))
proc = subprocess.run([sys.executable, "-B", RUNNER, "--kind", kind], cwd=repo, env=env,
capture_output=True, text=True, timeout=900)
env["WEB_FETCHER_PATH"] = str(ensure_web_fetcher())
# Per-run fetch cache outside the checkout: replayable, never committed.
result.cache_dir = tempfile.mkdtemp(prefix="assembly-checkpoint-cache-")
proc = subprocess.run([sys.executable, "-B", RUNNER, "--kind", kind, "--cache", result.cache_dir],
cwd=repo, env=env, capture_output=True, text=True, timeout=900)
summary = {}
for line in reversed(proc.stdout.strip().splitlines()):
try:
@ -146,7 +179,7 @@ def _hub(result: AssemblyCheckpointResult, report_to_hub: bool) -> None:
return
detail = {"repo": "assembly-economics", "date": result.date, "kind": result.kind, "path": result.path,
"out_of_window": result.out_of_window, "committed": result.committed, "pushed": result.pushed,
"origin_sha": result.origin_sha, **result.counts, "executor": "rein-aharness"}
"origin_sha": result.origin_sha, "cache_dir": result.cache_dir, **result.counts, "executor": "rein-aharness"}
if result.ok:
what = "out of window (no-op)" if result.out_of_window else f"recorded {result.path}"
hub.post_progress_event(summary=f"Assembly checkpoint {result.date} ({result.kind}): {what}",

View file

@ -7,11 +7,15 @@ from pathlib import Path
import pytest
from rein_aharness import approaches as ap
from rein_aharness.assembly_checkpoint import kind_from_title, run_assembly_checkpoint
from rein_aharness import assembly_checkpoint as ac
from rein_aharness.assembly_checkpoint import (AssemblyCheckpointError, ensure_web_fetcher, kind_from_title,
run_assembly_checkpoint)
RUNNER = textwrap.dedent('''
import json, sys, pathlib
kind = sys.argv[sys.argv.index("--kind") + 1]
cache = pathlib.Path(sys.argv[sys.argv.index("--cache") + 1])
assert cache.is_dir() and not cache.resolve().is_relative_to(pathlib.Path.cwd().resolve())
mode = pathlib.Path("checkpoints/MODE").read_text().strip()
if mode == "out":
print(json.dumps({"status": "out-of-window", "date": "2026-10-12", "kind": kind})); sys.exit(3)
@ -28,6 +32,12 @@ def _git(cwd, *args):
subprocess.run(["git", "-C", str(cwd), *args], check=True, capture_output=True)
@pytest.fixture(autouse=True)
def _web_fetcher_path(tmp_path, monkeypatch):
monkeypatch.setenv("WEB_FETCHER_PATH", str(tmp_path / "wf"))
monkeypatch.delenv("ASSEMBLY_WEB_FETCHER_PATH", raising=False)
@pytest.fixture
def repo(tmp_path: Path) -> Path:
origin = tmp_path / "origin.git"
@ -84,3 +94,34 @@ def test_label_selects_assembly_checkpoint():
labels=["assembly-checkpoint", "research", "automated"], target_repo="assembly-economics",
activity_definition_id="assembly-checkpoint-setup-daily")
assert ap.select_approach(OpsRun(**kw)) == ap.APPROACH_ASSEMBLY_CHECKPOINT
def _tagged_origin(tmp_path: Path, tag: str) -> Path:
src = tmp_path / "wf-src"
_git(tmp_path, "init", "-q", "-b", "main", str(src))
_git(src, "config", "user.email", "t@t"), _git(src, "config", "user.name", "t")
(src / "README").write_text("wf")
_git(src, "add", "-A"), _git(src, "commit", "-qm", "init"), _git(src, "tag", tag)
return src
def test_ensure_web_fetcher_clones_pinned_tag(tmp_path, monkeypatch):
monkeypatch.delenv("WEB_FETCHER_PATH")
monkeypatch.setenv("ASSEMBLY_WEB_FETCHER_URL", (_tagged_origin(tmp_path, ac.WEB_FETCHER_TAG)).as_uri())
dest = ensure_web_fetcher(tmp_path / "cache")
assert dest == tmp_path / "cache" / f"web-fetcher-{ac.WEB_FETCHER_TAG}" and (dest / "README").exists()
assert ensure_web_fetcher(tmp_path / "cache") == dest # reused, re-verified
def test_ensure_web_fetcher_refuses_drifted_checkout(tmp_path, monkeypatch):
monkeypatch.delenv("WEB_FETCHER_PATH")
monkeypatch.setenv("ASSEMBLY_WEB_FETCHER_URL", (_tagged_origin(tmp_path, ac.WEB_FETCHER_TAG)).as_uri())
dest = ensure_web_fetcher(tmp_path / "cache")
_git(dest, "config", "user.email", "t@t"), _git(dest, "config", "user.name", "t")
_git(dest, "commit", "-q", "--allow-empty", "-m", "drift")
with pytest.raises(AssemblyCheckpointError, match="not v0.2.0"):
ensure_web_fetcher(tmp_path / "cache")
def test_explicit_web_fetcher_path_wins(tmp_path):
assert ensure_web_fetcher(tmp_path / "cache") == tmp_path / "wf"