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.
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.