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

ACTIVE#422 forge-refactor

Rewrite agent_tools_suite:crank_policy.py as behavior-preserving, installable, control-config'd Python [forge-rw:e3353e4979372e68]

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:e3353e4979372e68 (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 : 4625 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : agent_tools_suite path : crank_policy.py ref : HEAD (HEAD) blob : 85ba055f571b705e319c2952f9582ba301d4c18c (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 85ba055f571b705e319c2952f9582ba301d4c18c 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_policy.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 — 4625 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_policy — the HUMAN files a WALLET-SIGNED standing crank policy: authorize an agent to crank a component autonomously within bounds (max_version, expiry). crankd verifies the signature + bounds before each crank; the audit-veto still confines every patch. Running this IS the standing authorization — it replaces per-crank sign-off within the scope you set. python crank_policy.py <component> --max-version 0.10 [--days 7] [--yes] """ 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_policy.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 hashlib import json import os import subprocess import sys import tempfile import urllib.request from datetime import datetime, timedelta, timezone from cryptography.hazmat.primitives.serialization import load_ssh_private_key, load_ssh_public_key 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 policy_digest(component, max_version, expires): """Must match crankd.policy_digest: sha256 over component, max_version, expires (in that order).""" canon = "\n".join(str(x) for x in (component, max_version, expires)) return hashlib.sha256(canon.encode()).hexdigest() def sign_digest(digest_str): 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): 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 _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 main(): ap = argparse.ArgumentParser(description="file a wallet-signed standing crank policy") ap.add_argument("component") ap.add_argument("--max-version", required=True, help="crank allowed while current version < this") ap.add_argument("--days", type=int, default=0, help="expiry in days (0 = no expiry)") ap.add_argument("--yes", action="store_true", help="skip the confirmation prompt") args = ap.parse_args() expires = 0 if args.days <= 0 else int((datetime.now(timezone.utc) + timedelta(days=args.days)).timestamp()) digest = policy_digest(args.component, args.max_version, expires) print(f" component : {args.component}") print(f" max_version : {args.max_version} (crank allowed while current < this)") print(f" expires : {expires or 'never'}") print(f" digest : {digest}") if not args.yes: if input("\n wallet-sign this standing policy? [y/N] ").strip().lower() != "y": sys.exit(" aborted — no policy filed") 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=policy.create", {"component": args.component, "max_version": args.max_version, "expires": expires, "sig": sig}) print(f"\n ✓ policy filed — '{args.component}' cranks run autonomously while version < {args.max_version}" f"{' (verified locally)' if ok else ''}") 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) policy_digest(component, max_version, expires) sign_digest(digest_str) verifies_locally(digest_str, sig_hex) _cong_post(path, obj) 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.

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)

checkouts/current/forge2/agent_tools_suite/crank_policy.py:f093a8abdd forge-source · forge 2519caccfc74
checkouts/current/forge2/agent_tools_suite/pyproject.toml:f093a8abdd forge-source · forge 9320578285b4
checkouts/current/forge2/agent_tools_suite/registry.json:f093a8abdd forge-source · forge 3d33317a3cab

source (forge tree)

browse congruency source (forge-parked tree)

children

not yet recorded — lands with the ticket-class build