Ticket graphs

Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.

← all ticket-graphs

REQUESTED#425 forge-refactor

Rewrite agent_tools_suite:crankimg-mcp/server.py as behavior-preserving, installable, control-config'd Python [forge-rw:137ce3dfed322b2a]

goal

Rewrite ONE unique forge module as entirely new, behavior-preserving, installable, control-config'd Python, per the FORGE-wide refactor standard #200 (#std). This ticket covers ONE module — the one identified below — and nothing else. It was filed against the module's DEDUPLICATED identity, so if that module was copy-pasted across the forge, every copy is covered by this single ticket. MODULE KEY : forge-rw:137ce3dfed322b2a (the ticketer's idempotency marker — it lives in this ticket's TITLE. Do not edit the title, or the module will be re-filed as a duplicate.) IDENTITY : AST-NORMALIZED — codebean's `_ast_norm` (#183), i.e. ast.dump(ast.parse(ast.unparse(ast.parse(src)))). Copies of this module that differ only in comments, blank lines, quoting or spacing carry the SAME module-sha and are covered by THIS ticket. A differing docstring is real content, so a module whose docstring differs is a DIFFERENT module with its own ticket. SIZE : 9947 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : agent_tools_suite path : crankimg-mcp/server.py ref : HEAD (HEAD) blob : effecd5393c1fdf8f7a247b7f20e47296fc772f5 (git blob sha1) REFERENCE ONLY — you do NOT need this command. This module's bytes are embedded COMPLETE in THE ORIGINAL SOURCE block below, and that is what the rewrite is based on. For a reader who does have forge access: git --git-dir=/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes/agent_tools_suite.git cat-file blob effecd5393c1fdf8f7a247b7f20e47296fc772f5 THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS: none — this module is unique to agent_tools_suite among the repos scanned so far. HISTORICAL OCCURRENCES of this exact blob — every OTHER DISTINCT {repo, path} the omni-git index (`occ`, joined on the git blob sha1) records this byte-identical file at, with the index's own `ts` for the first row of each. Any place this run enumerated LIVE is listed under the forge tips above, not here. Context for the rewrite, not extra work: testmonkeyalpha-group/jazz-forge/agent_tools_suite:crankimg-mcp/server.py [occ.ts gitlab] THE ORIGINAL SOURCE, EMBEDDED — YOU NEED NO FORGE ACCESS TO DO THIS TICKET. The module's own bytes are reproduced below, inside this ticket. THEY are the authoritative copy and the behavior-preserving basis: rewrite from them. The `git --git-dir=... cat-file blob` command under CANONICAL SOURCE above is kept only as a REFERENCE for a reader who happens to have forge access — running it is NOT part of this ticket. (Why the bytes are here at all: the pair_loop worker that executes this ticket is sandboxed to the ticket's own congruency worktree and cannot read the forge's bare repos. Ticket #346 is the fix that put them on the ticket.) The block BEGINS at the line ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== and ENDS at the first line ===END SOURCE=== Everything strictly between those two lines is the module — 9947 byte(s), byte for byte, with no re-indentation, no re-wrapping and nothing elided. ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== #!/usr/bin/env python3 """crankimg — the ENTIRE tool surface of a single-purpose "crank a Pi image" agent. Five gated verbs over the crank_pi drivers. The agent can: build an image, verify it, list flashable disks, prepare a guarded flash, and ensure the base image — and NOTHING else. There is no bash, no file edit, no git, no network beyond `base`. The one destructive act (flash) is PREPARED here and handed to ~/code/confirm.sh for human review + sudo — the agent never runs `dd` itself. Owned by the coupler, so every call flows through the gate and carries a logged intent. """ import hashlib import os import re import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) # ~/code/agent_tools_suite, where mcplib lives import mcplib # noqa: E402 HOME = os.path.expanduser("~") DRIVERS = os.path.join(HOME, "code", "crank_pi", "drivers") IMG = os.path.join(HOME, "bin", "config", "vm", "raspios-trixie-arm64-lite.img") SEEDDIR = os.path.join(HOME, "bin", "config", "vm", "pi-forge", "seed") BASE_URL = ("https://downloads.raspberrypi.com/raspios_lite_arm64/images/" "raspios_lite_arm64-2026-06-19/2026-06-18-raspios-trixie-arm64-lite.img.xz") BINPATH = HOME + "/bin:/opt/homebrew/bin:" + os.environ.get("PATH", "") def _run(cmd, env=None, **kw): e = dict(os.environ, PATH=BINPATH) if env: e.update(env) return subprocess.run(cmd, capture_output=True, text=True, env=e, **kw) # ── build ───────────────────────────────────────────────────────────────────── def t_build(a): env = {"NODE": a.get("node", "pi-forge"), "SSID": a.get("ssid", "WIFI-B61C"), "WIFI_SECRET": a.get("wifi_secret", "WIFI-B61C")} r = _run(["bash", os.path.join(DRIVERS, "build_pi_forge.sh")], env=env) out = (r.stdout + r.stderr).strip() ok = "ready to flash" in r.stdout tail = "\n".join(out.splitlines()[-10:]) if not ok and "isn't in the wallet yet" in out: return mcplib.text_result("✗ Wi-Fi passphrase not in the wallet — a human must store it " "once: `wallet add %s`. Then re-run build.\n%s" % (env["WIFI_SECRET"], tail), True) return mcplib.text_result(("✓ image built + baked (login + fleet key + Wi-Fi PSK)\n" if ok else "✗ build failed\n") + tail, not ok) # ── verify (read-only image inspection) ──────────────────────────────────────── def t_verify(a): ud = os.path.join(SEEDDIR, "user-data") nc = os.path.join(SEEDDIR, "network-config") if not os.path.exists(ud): return mcplib.text_result("✗ no baked seed at %s — run build first" % SEEDDIR, True) checks = [] udt = open(ud).read() checks.append(("hostname/login present", "hostname:" in udt and "chpasswd" in udt)) checks.append(("fleet ssh key present", "ssh-ed25519" in udt)) if os.path.exists(nc): nct = open(nc).read() psk = re.search(r"[0-9a-f]{64}", nct) checks.append(("Wi-Fi PSK derived (not placeholder)", bool(psk) and "INSERT_WIFI_PASSWORD_HERE" not in nct)) ok = all(v for _, v in checks) body = "\n".join(" %s %s" % ("✓" if v else "✗", k) for k, v in checks) return mcplib.text_result(("✓ image verified\n" if ok else "✗ verification failed\n") + body, not ok) # ── disks (classify targets; refuse the dangerous ones) ──────────────────────── def _disk_verdict(dev): info = _run(["diskutil", "info", dev]).stdout if not info: return "unknown", "not found" loc = (re.search(r"Device Location:\s*(.+)", info) or [None, ""])[1].strip() if dev == "/dev/disk0": return "REFUSE", "system disk" if "External" not in loc: return "REFUSE", "internal (Location=%s)" % (loc or "?") if re.search(r"backup", _run(["diskutil", "list", dev]).stdout, re.I): return "REFUSE", "carries a 'backup' volume" sz = (re.search(r"Disk Size:\s*(.+?) \(", info) or [None, "?"])[1] return "candidate", "external %s" % sz def t_disks(a): devs = re.findall(r"^(/dev/disk\d+)", _run(["diskutil", "list"]).stdout, re.M) lines = [] for d in devs: verdict, why = _disk_verdict(d) mark = "✓" if verdict == "candidate" else "🚫" lines.append(" %s %-12s %-9s %s" % (mark, d, verdict, why)) return mcplib.text_result("flashable targets (🚫 = refused):\n" + "\n".join(lines)) # ── flash (PREPARE only — writes the reviewed job, hands to confirm.sh) ───────── def t_flash(a): dev = (a.get("device") or "").strip() if not dev: return mcplib.text_result("device required — e.g. device='/dev/disk9' (see crankimg__disks)", True) if a.get("confirm") != dev: return mcplib.text_result("refused: `confirm` must echo the device EXACTLY (confirm='%s')" % dev, True) verdict, why = _disk_verdict(dev) if verdict != "candidate": return mcplib.text_result("🚫 refused %s — %s. This is a guard; pick a candidate from crankimg__disks." % (dev, why), True) if not os.path.exists(IMG): return mcplib.text_result("✗ no image to flash — run crankimg__build first", True) rdev = "/dev/r" + os.path.basename(dev) job = ("""#!/usr/bin/env bash # disk_job.sh — WRITTEN BY crankimg (agent-prepared, human-confirmed via confirm.sh). set -euo pipefail IMG="%s"; DEV="%s"; RDEV="%s" [ -f "$IMG" ] || { echo "no image"; exit 1; } diskutil info "$DEV" | grep -qi 'Device Location.*External' || { echo "REFUSING $DEV (not External)"; exit 1; } [ "$DEV" = /dev/disk0 ] && { echo "REFUSING disk0"; exit 1; } diskutil list "$DEV" | grep -qi backup && { echo "REFUSING $DEV (backup volume)"; exit 1; } echo " target (WILL BE ERASED):"; diskutil list "$DEV" | sed 's/^/ /' diskutil unmountDisk "$DEV" sudo dd if="$IMG" of="$RDEV" bs=4m status=progress sync; diskutil eject "$DEV" echo " ✓ flashed $IMG → $DEV" """ % (IMG, dev, rdev)) open(os.path.join(DRIVERS, "disk_job.sh"), "w").write(job) return mcplib.text_result( "✓ flash PREPARED for %s (%s). I do NOT run dd.\n" " A human reviews + confirms + sudo:\n ~/code/confirm.sh\n" " (it pages the exact disk_job.sh I wrote, then two-gate confirms, then flashes.)" % (dev, why)) # ── base (ensure the RPi OS base image is present) ───────────────────────────── def t_base(a): if os.path.exists(IMG): return mcplib.text_result("✓ base image present: %s (%d MB)" % (IMG, os.path.getsize(IMG) // 1048576)) if not a.get("fetch"): return mcplib.text_result("base image MISSING at %s. Call again with fetch=true to download (~525 MB xz)." % IMG, True) os.makedirs(os.path.dirname(IMG), exist_ok=True) xz = IMG + ".xz" r = _run(["curl", "-sL", "-o", xz, BASE_URL]) if r.returncode: return mcplib.text_result("✗ download failed: %s" % r.stderr[-200:], True) d = _run(["sh", "-c", "xz -dkf %s" % xz]) return mcplib.text_result(("✓ base image fetched + decompressed → %s" % IMG) if os.path.exists(IMG) else "✗ decompress failed: %s" % d.stderr[-200:], not os.path.exists(IMG)) TOOLS = [ {"name": "build", "description": "Build + bake a Pi image: cloud-init seed (console login + wallet 'fleet' ssh key + Wi-Fi WPA-PSK derived from a wallet secret) onto the RPi OS base image. The agent never sees the passphrase.", "inputSchema": {"type": "object", "properties": { "node": {"type": "string", "description": "hostname (default pi-forge)"}, "ssid": {"type": "string", "description": "Wi-Fi SSID (default WIFI-B61C)"}, "wifi_secret": {"type": "string", "description": "wallet secret name holding the Wi-Fi passphrase (default WIFI-B61C)"}}}}, {"name": "verify", "description": "Read-only check that the last-built image is correct: login/hostname present, fleet ssh key present, Wi-Fi PSK derived (not a placeholder). Run before flashing.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "disks", "description": "List attached disks with a per-disk VERDICT: 'candidate' (external) or 'REFUSE' (system disk / internal / a backup volume). Read-only recon before flashing.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "flash", "description": "PREPARE a guarded flash of the built image to a named external disk. Requires `confirm` to echo `device` exactly; refuses disk0/internal/backup. Writes the reviewed job and hands off to ~/code/confirm.sh — the agent never runs dd.", "inputSchema": {"type": "object", "properties": { "device": {"type": "string", "description": "target, e.g. /dev/disk9 (from crankimg__disks)"}, "confirm": {"type": "string", "description": "must equal `device` exactly"}}, "required": ["device", "confirm"]}}, {"name": "base", "description": "Ensure the RPi OS 64-bit Lite base image is present; with fetch=true, download it (~525 MB).", "inputSchema": {"type": "object", "properties": {"fetch": {"type": "boolean"}}}}, ] DISPATCH = {"build": t_build, "verify": t_verify, "disks": t_disks, "flash": t_flash, "base": t_base} def list_tools(): return TOOLS def call_tool(name, args): fn = DISPATCH.get(name) if not fn: return mcplib.text_result("crankimg has no tool %r" % name, True) return fn(args or {}) if __name__ == "__main__": # importable (validate.py drives call_tool) without serving mcplib.Server("crankimg", "1.0.0", list_tools, call_tool).serve() ===END SOURCE=== THE CALLABLE SURFACE — every callable in the original, lowered with codebean's `lower_node` (#183). The rewrite must present THIS surface (same names, same parameters, same defaults) so callers of the original keep working: _run(cmd, env=None, **kw) t_build(a) t_verify(a) _disk_verdict(dev) t_disks(a) t_flash(a) t_base(a) list_tools() call_tool(name, args) CAPTURED BEHAVIORAL EXEMPLARS: none. no function in this module is minable — criteria_miner mines only self-contained pure functions (it refuses anything that mentions a world-touching or non-deterministic name, plus methods and no-arg functions), and nothing here qualified This does NOT weaken the ticket's bar — it MOVES the work: you must write the equivalence exemplars by hand from the original's behavior before rewriting, and they are what settles this ticket. THE STANDARD — #200's four requirements, which this ticket exists to enforce. All four are the standard's own words; none is optional: 1. BEHAVIOR-PRESERVING, VERIFIED NOT ASSERTED — the deliverable is ENTIRELY NEW source code (rewritten, not copied). It must reproduce the ORIGINAL's behavior, and the proof is the exemplars above (captured via codebean lower_* #183 + criteria_miner IO-exemplars, exactly as #200 specifies). 2. A PYTHON MODULE — regardless of the source language. This module's source is `.py`; the rewrite is Python either way. 3. INSTALLABLE — a proper package: pyproject.toml, `pip install -e .` works, console entry points where applicable. WRITE ONLY YOUR OWN MODULE FILE at forge2/<repo>/<module>.py. Do NOT hand-author pyproject.toml or __init__.py: the harness DERIVES them ADDITIVELY from the modules present (congruency/tools/ forge_package.py, #410 — the analogue of the tag-registry fix #281), so your unit never conflicts with, or clobbers, a sibling module of the same forge2/<repo>/ package. A package-level pyproject includes every .py in the directory, so your module installs and imports as `<repo>.<module>` the moment it lands. 4. CONTROL-CONFIG'D — NO hardcoded paths or params. A JSON/registry config object drives it (the install.json / registry.json idiom of this repo), and per note-for-claude a tool that cannot see its config THROWS rather than guessing a default. Add ONLY your own `forge2.<repo>.<module>` section to the package's registry.json (create the file if it is absent); NEVER read, edit or drop a sibling module's section — the sections are disjoint keys and stay additive that way. ACCEPTANCE (#200's, per rewrite ticket) — this ticket settles when ALL of: a. the new source reproduces every captured exemplar above, plus the edge cases you add for what sampling cannot reach (paste the runs as evidence); b. it presents the callable surface above, so existing callers keep working; c. `pip install -e .` installs it clean from its own pyproject.toml; d. it runs off a config object with ZERO hardcoded settings, and throws without one. Kind is `build` (a pair: coder + adversarial tester). This ticket hangs off #200 by a `spawned` edge, so a stop ticket aimed at #200 brakes every rewrite in this programme. Filed mechanically by checkouts/current/congruency/tools/forge_refactor_ticketer.py under ticket #201, from the forge bare repos at /Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes. Its original source is embedded above (#346), so this ticket is complete on its own.

patch

none

approval

unapproved

runs

no runs recorded

mandate (clauses)

not yet recorded — lands with the ticket-class build

manifestations

not yet recorded — lands with the ticket-class build

guarantees

not yet recorded — lands with the ticket-class build

journey (blunders & successes)

not yet recorded — lands with the ticket-class build

artifacts (forge)

not yet recorded — lands with the ticket-class build

source (forge tree)

browse congruency source (forge-parked tree)

children

not yet recorded — lands with the ticket-class build