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:2aa9706d7f7163b0
(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 : 4882 bytes
EXTENSION : .py
CANONICAL SOURCE (the location this ticket is named for):
repo : agent_tools_suite
path : crank_approve.py
ref : HEAD (HEAD)
blob : 67367c32c83e78ca4a79da7ce3ad6926cf950cae (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 67367c32c83e78ca4a79da7ce3ad6926cf950cae
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:crank_approve.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 — 4882 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
"""crank_approve — the HUMAN side of crank approval: wallet-sign a ticket's content digest so crankd
will run it. The agent cannot do this (it has no wallet); running this IS the approval.
python crank_approve.py <component> [--yes]
It reads the ticket's content digest from crankd (the single digest authority, so there's no drift),
shows the ticket, signs that digest with the wallet's 'crank-approver' key, verifies the signature
locally, and PUTs it to congruency ticket.approve. crankd then re-checks that signature against the
wallet pubkey before running the crank.
"""
import os
import sys
# self-heal: this tool needs the jail venv (cryptography). Re-exec under it if invoked with another
# interpreter (e.g. `python3 crank_approve.py`).
_JAIL_PY = os.path.expanduser("~/jail/.venv/bin/python")
if os.path.exists(_JAIL_PY) and os.path.realpath(sys.executable) != os.path.realpath(_JAIL_PY):
os.execv(_JAIL_PY, [_JAIL_PY, os.path.abspath(__file__), *sys.argv[1:]])
import argparse
import json
import subprocess
import tempfile
import urllib.request
from cryptography.hazmat.primitives.serialization import load_ssh_private_key, load_ssh_public_key
CRANKD = os.environ.get("CRANKD_URL", "http://127.0.0.1:8812")
CONG = (os.environ.get("CONGRUENCY_URL") or "http://127.0.0.1:8899").rstrip("/")
WALLET = os.environ.get("WALLET_BIN") or os.path.expanduser("~/bin/wallet")
APPROVER = "crank-approver"
def _secret(name):
path = os.path.expanduser("~/.secrets/ats.env")
if os.path.isfile(path):
for line in open(path):
line = line.strip()
if line.startswith(name + "="):
return line.split("=", 1)[1]
return os.environ.get(name)
CONGRUENCY_KEY = _secret("CONGRUENCY_API_KEY")
APPROVER_PUBKEY = _secret("CRANK_APPROVER_PUBKEY")
def _get(url):
with urllib.request.urlopen(url, timeout=5) as r:
return json.loads(r.read().decode() or "{}")
def _cong_post(path, obj):
headers = {"Content-Type": "application/json"}
if CONGRUENCY_KEY:
headers["X-Api-Key"] = CONGRUENCY_KEY
req = urllib.request.Request(CONG + path, data=json.dumps(obj).encode(), method="POST", headers=headers)
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read().decode() or "{}")
def sign_digest(digest_str):
"""Sign the digest string with the wallet-derived crank-approver ed25519 key; return hex."""
with tempfile.TemporaryDirectory() as d:
keyfile = os.path.join(d, "k")
subprocess.run([WALLET, "ssh", APPROVER, "-w", keyfile], check=True, capture_output=True)
with open(keyfile, "rb") as fh:
key = load_ssh_private_key(fh.read(), password=None)
return key.sign(digest_str.encode()).hex()
def verifies_locally(digest_str, sig_hex):
"""True/False if the signature checks against the approver pubkey; None if no pubkey configured."""
if not APPROVER_PUBKEY:
return None
try:
load_ssh_public_key(APPROVER_PUBKEY.encode()).verify(bytes.fromhex(sig_hex), digest_str.encode())
return True
except Exception: # noqa: BLE001
return False
def main():
ap = argparse.ArgumentParser(description="wallet-sign a crank ticket so crankd will run it")
ap.add_argument("component")
ap.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
args = ap.parse_args()
info = _get(f"{CRANKD}/digest?component={args.component}")
if "digest" not in info:
sys.exit(f"no approvable ticket for {args.component!r}: {info.get('error', info)}")
ticket_id, digest = info["ticket"], info["digest"]
ticket = (_get(f"{CONG}/?route=ticket.approval&component={args.component}").get("rows") or [{}])[0]
print(f" component : {args.component}")
print(f" ticket : {ticket_id}")
print(f" title : {ticket.get('title', '')}")
print(f" body : {ticket.get('body', '')}")
patch = ticket.get("patch") or ""
if patch.strip():
print(" patch |\n" + "\n".join(" " + line for line in patch.splitlines()))
print(f" digest : {digest}")
if not args.yes:
if input("\n wallet-sign and approve this ticket? [y/N] ").strip().lower() != "y":
sys.exit(" aborted — not approved")
sig = sign_digest(digest)
ok = verifies_locally(digest, sig)
if ok is False:
sys.exit(" ✗ the produced signature does NOT verify against the approver pubkey — aborting")
_cong_post("/?route=ticket.approve", {"id": ticket_id, "approval_sig": sig})
print(f"\n ✓ approved ticket {ticket_id} — signature stored"
f"{' (verified locally)' if ok else ''}")
print(f" crankd will now run it: POST {CRANKD}/crank {{\"component\":\"{args.component}\",\"intent\":\"...\"}}")
if __name__ == "__main__":
main()
===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:
_secret(name)
_get(url)
_cong_post(path, obj)
sign_digest(digest_str)
verifies_locally(digest_str, sig_hex)
main()
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.