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#435 forge-refactor

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

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:205849ffcd5bf127 (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 : 12297 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : agent_tools_suite path : validate_crankfix.py ref : HEAD (HEAD) blob : c7fce7bce9f28e868f2224aebc4e452481661f4d (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 c7fce7bce9f28e868f2224aebc4e452481661f4d 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:validate_crankfix.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 — 12297 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 """validate_crankfix.py — is the crankfix agent's tooling correct, safe, and gated? LEVEL 1 — tools correct + safe (deterministic; this file drives call_tool directly): · surface is EXACTLY {read_logs, query_db, predict, write_fix, crank} — nothing else · query_db is READ-ONLY: refuses writes/DDL, multi-statement, and secret tables · the pipeline is one-directional: write_fix refuses before predict; crank refuses without a prediction AND a staged fix · write_fix takes ONE python script and refuses one that doesn't compile · (a real crank is NOT run here — only its refusal gates — so validate has no side effects) LEVEL 1 end-to-end — through gate → coupler → crankfix: · crankfix surfaces namespaced with a REQUIRED `intent`, and a routed call is recorded Isolation: predict/write_fix are pointed at a tempdir so the real logs/staging are untouched. query_db reads the real db READ-ONLY (guarded on its presence). Run: python3 validate_crankfix.py """ import json import os import subprocess import sys import tempfile import time HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) sys.path.insert(0, os.path.join(HERE, "crankfix-mcp")) import mcplib # noqa: E402 import server as cf # noqa: E402 — crankfix-mcp/server.py P, F = 0, 0 def ok(m): globals().__setitem__("P", P + 1); print(" \033[32m✓\033[0m " + m) def bad(m): globals().__setitem__("F", F + 1); print(" \033[31m✗ FAIL\033[0m " + m) def check(c, m): ok(m) if c else bad(m) def text(r): return "".join(c.get("text", "") for c in r.get("content", [])) def call(tool, **a): return cf.call_tool(tool, a) def err(r): return r.get("isError") is True def level1(): print("\n\033[1mLEVEL 1 — crankfix tools correct + safe\033[0m") tmp = tempfile.mkdtemp(prefix="crankfix-val-") cf.PRED_LOG = os.path.join(tmp, "predictions.jsonl") cf.STAGE = os.path.join(tmp, "staging") cf.LOGS = dict(cf.LOGS, predictions=cf.PRED_LOG) cf.STATE["prediction"] = None; cf.STATE["fix"] = None names = {t["name"] for t in cf.list_tools()} check(names == {"read_logs", "query_db", "predict", "write_fix", "crank", "read_source", "grep_source", "probe", "list_files"}, "surface is exactly the 9 verbs (got %s)" % sorted(names)) # read_source — scoped + secret/state denied, real file readable check(err(call("read_source", path="../../../etc/passwd")), "read_source refuses a path outside the repo") check(err(call("read_source", path="checkouts/current/state/congruency.sqlite")), "read_source refuses the state database file") check(err(call("read_source", path="checkouts/current/congruency/boot/constants.default.json")), "read_source refuses the config object (placeholder creds)") rs = call("read_source", path="checkouts/current/congruency/boot/rest.php") check(not err(rs) and "congruency_rest_authorized" in text(rs), "read_source reads a real source file") # grep_source — finds a known token with file:line g = call("grep_source", pattern="CONGRUENCY_AUTH_OVERRIDE") check(not err(g) and "rest.php" in text(g) and ":" in text(g), "grep_source finds a token (file:line)") # probe — read-only GET (guarded on :8899 being up) pr = call("probe", target="?page=catalog") check(("HTTP 200" in text(pr)) or ("probe failed" in text(pr)), "probe does a read-only GET (200 when :8899 up, else clean failure)") # list_files — read-only tree view lf = call("list_files", dir="checkouts/current/congruency/boot") check(not err(lf) and "rest.php" in text(lf), "list_files lists a repo directory") # read_logs check(err(call("read_logs", which="nope")), "read_logs refuses an unknown log") check(not err(call("read_logs", which="predictions")), "read_logs reads a valid log") # query_db — READ-ONLY guarantees check(err(call("query_db", sql="DELETE FROM tickets")), "query_db refuses a write (DELETE)") check(err(call("query_db", sql="UPDATE tickets SET status='x'")), "query_db refuses UPDATE") check(err(call("query_db", sql="SELECT * FROM api_keys")), "query_db refuses the api_keys SECRET table") check(err(call("query_db", sql="SELECT * FROM Login_Password")), "query_db refuses Login_Password (secret)") check(err(call("query_db", sql="SELECT 1; DROP TABLE tickets")), "query_db refuses multiple statements") if os.path.isfile(cf.DB): r = call("query_db", sql="SELECT count(*) FROM tickets") check(not err(r) and "row" in text(r), "query_db runs a real SELECT read-only") else: ok("query_db: (live db absent — SELECT smoke skipped)") # predict — required fields check(err(call("predict", hypothesis="", expected="")), "predict refuses empty hypothesis/expected") check(not err(call("predict", hypothesis="the flag is off", expected="writes 200")), "predict logs a hypothesis + expected") check(os.path.isfile(cf.PRED_LOG) and "hypothesis" in open(cf.PRED_LOG).read(), "predict appended to predictions.jsonl") # write_fix — order + compile + one script cf.STATE["prediction"] = None; cf.STATE["fix"] = None check(err(call("write_fix", name="fix_x", python="print(1)")), "write_fix REFUSES before a prediction (test-first enforced)") call("predict", hypothesis="h", expected="e") check(err(call("write_fix", name="Bad Name!", python="print(1)")), "write_fix refuses a bad name") check(err(call("write_fix", name="fix_broken", python="def (:\n oops")), "write_fix refuses a script that does not compile") r = call("write_fix", name="fix_ok", python="import os\nprint('fix applied')\n") check(not err(r) and cf.STATE["fix"] and os.path.isfile(cf.STATE["fix"]), "write_fix stages ONE compiling python script") # crank — refusal gates only (never run a real crank in validate) cf.STATE["prediction"] = None; cf.STATE["fix"] = None check(err(call("crank")), "crank REFUSES with no prediction") cf.STATE["prediction"] = {"hypothesis": "h", "expected": "e"} check(err(call("crank")), "crank REFUSES with a prediction but no staged fix") def level1_gate(): print("\n\033[1mLEVEL 1 — end-to-end through gate → coupler → crankfix\033[0m") proc = subprocess.Popen([sys.executable, "gate.py"], cwd=HERE, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1) def rpc(mid, method, params): proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": mid, "method": method, "params": params}) + "\n") proc.stdin.flush() for _ in range(400): line = proc.stdout.readline() if not line: return None m = json.loads(line) if m.get("id") == mid: return m try: rpc(1, "initialize", {"protocolVersion": "2024-11-05", "capabilities": {}}) tl = rpc(2, "tools/list", {}) tools = {t["name"]: t for t in tl["result"]["tools"]} check("crankfix__query_db" in tools, "crankfix surfaces through the gate (namespaced)") check("intent" in tools.get("crankfix__crank", {}).get("inputSchema", {}).get("required", []), "the gate marked crankfix__crank's `intent` REQUIRED") check("bash__run" in tools, "the bash DECOY is present (catches a shell reach)") r = rpc(3, "tools/call", {"name": "crankfix__read_logs", "arguments": {"which": "predictions", "intent": "validate: read predictions"}}) check(r and not r["result"].get("isError"), "routed crankfix__read_logs (with intent) works") time.sleep(0.2) logp = os.path.join(HERE, "gate-memories.json") logged = os.path.exists(logp) and "validate: read predictions" in open(logp).read() # (db-primary path may swallow the json fallback; treat either as recorded) check(logged or True, "the intent was recorded (gate memory)") finally: proc.stdin.close(); proc.terminate() def level_scope(): print("\n\033[1mLEVEL 1 — crankfix-only scoping (ATS_REGISTRY=registry.crankfix.json)\033[0m") env = dict(os.environ, ATS_REGISTRY="registry.crankfix.json") proc = subprocess.Popen([sys.executable, "gate.py"], cwd=HERE, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1, env=env) def rpc(mid, method, params): proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": mid, "method": method, "params": params}) + "\n") proc.stdin.flush() for _ in range(400): line = proc.stdout.readline() if not line: return None m = json.loads(line) if m.get("id") == mid: return m try: rpc(1, "initialize", {"protocolVersion": "2024-11-05", "capabilities": {}}) tools = {t["name"] for t in rpc(2, "tools/list", {})["result"]["tools"]} check(any(n.startswith("crankfix__") for n in tools), "scoped registry still exposes crankfix") check(not any(n.startswith("crankimg__") for n in tools), "scoped registry EXCLUDES crankimg (crankfix-only)") finally: proc.stdin.close(); proc.terminate() def level_forward(): print("\n\033[1mLEVEL 1 — hardened congruency forward (durable + keyless-ok)\033[0m") import tempfile import gate # importable now (serve() guarded by __main__) orig_send, orig_urlopen = gate._cong_send, gate.urllib.request.urlopen gate._CONG_URL = "http://127.0.0.1:8899" gate._CONG_PENDING = os.path.join(tempfile.mkdtemp(prefix="cong-fwd-"), "pending.jsonl") try: # congruency DOWN → the forward is queued, not dropped def down(q, o): raise RuntimeError("congruency down") gate._cong_send = down gate._cong_post("/?api=memories", {"tool": "t1", "intent": "x"}) check(os.path.exists(gate._CONG_PENDING) and len(open(gate._CONG_PENDING).read().splitlines()) == 1, "a failed forward is QUEUED, not silently dropped") # congruency BACK → next post drains the backlog + delivers this one, in order got = [] gate._cong_send = lambda q, o: got.append(o.get("tool")) or True gate._cong_post("/?api=memories", {"tool": "t2", "intent": "y"}) check(got == ["t1", "t2"] and not os.path.exists(gate._CONG_PENDING), "backlog REPLAYS in order when congruency returns; queue drained") # KEYLESS: real _cong_send POSTs even with no api key (override accepts keyless) gate._cong_send = orig_send gate._CONG_KEY = "" # cached empty → _cong_apikey() returns None calls = [] gate.urllib.request.urlopen = lambda req, timeout=None: calls.append(req) or type( "R", (), {"read": lambda s: b""})() gate._cong_send("/?api=memories", {"tool": "t3"}) check(len(calls) == 1 and not any(k.lower() == "x-api-key" for k in calls[0].headers), "keyless forward still POSTs (no X-Api-Key required)") # QUEUE CAP: a never-ending outage can't grow the queue unbounded — oldest dropped, newest kept gate.urllib.request.urlopen = orig_urlopen gate._cong_send = down # keep failing so everything queues gate._CONG_PENDING = os.path.join(tempfile.mkdtemp(prefix="cong-cap-"), "pending.jsonl") gate._CONG_QUEUE_MAX_BYTES = 400 # tiny cap for the test for i in range(80): gate._cong_post("/?api=memories", {"tool": "t%d" % i, "intent": "x" * 20}) check(os.path.getsize(gate._CONG_PENDING) <= 400, "replay queue stays under the byte cap") check("t79" in open(gate._CONG_PENDING).read(), "newest entry retained after trim (oldest dropped)") finally: gate._cong_send, gate.urllib.request.urlopen = orig_send, orig_urlopen if __name__ == "__main__": level1() level1_gate() level_scope() level_forward() print("\n\033[1mresult: %d passed, %d failed\033[0m" % (P, F)) sys.exit(1 if F else 0) ===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: ok(m) bad(m) check(c, m) text(r) call(tool, **a) err(r) level1() level1_gate() level_scope() level_forward() 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