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