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:
parent
b80096133f
commit
dcf444f001
3 changed files with 81 additions and 7 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue