textmachine/eval/cold_run_b/door.py

508 lines
26 KiB
Python
Executable file

#!/usr/bin/env python3
"""door.py — the driver of cold run B: one book from the intake door to a downloaded file.
Why this exists next to `eval/door_to_file/drive.sh` rather than as an edit of it: that harness is
frozen evidence of run A, and run A named two defects in it that this run cannot inherit.
* IT PRINTED EVERY HTTP ANSWER TO THE SCREEN AND WROTE NONE TO DISK. Five of A's pre-registration
rows ended with no carrier at all (§10.12 of its report): the `run-options` body, the 202 of the
start, the corrections door's `changed`/`applied`, and a whole finding that consisted of nothing
but a response body. Here EVERY request and EVERY answer is appended to evidence/http/ before
the caller sees it, so an assertion about a body can be re-read rather than remembered.
* ITS LIVENESS WATCH READ max(last_seq) ACROSS ATTEMPTS, so during the second attempt the number
looked frozen at the first attempt's value while work was going on. Here every liveness number
is read PER ATTEMPT, and the attempt is taken from the row, never inferred from time.
Two rules this file keeps because breaking them is what makes a run measure nothing:
* money is read filtered by ONE account. A's collector summed the ledger over every account on the
stand and reported two grants as one (§14.2); the smoke's account and the paid account must never
meet in a number.
* a claim about a count prints its control quantity beside it. "0 rows" and "no such table" look
the same in output, and in money that is the difference between "did not spend" and "did not look".
"""
import argparse, http.cookiejar, json, mimetypes, os, re, subprocess, sys, time, urllib.error, urllib.request, uuid
W = os.environ.get("W", "/home/ubuntu-26/tm-coldrun-b")
ADDR = os.environ.get("ADDR", "127.0.0.1:8098")
DSN = os.environ.get("TM_PLATFORM_DSN", "postgres://postgres@/tm_coldrun_b?host=/tmp&port=55433&sslmode=disable")
PSQL = os.path.expanduser("~/.local/pgsql/bin/psql")
HTTPDIR = os.path.join(W, "evidence", "http")
STATE = os.path.join(W, "stand", "run-state.json")
# An opener that does NOT go through the environment's proxy: this host exports a webshare proxy and
# a WinINET-style NO_PROXY that Go, curl and urllib all fail to parse, so a request to 127.0.0.1 goes
# out to the proxy and comes back 403 (provider-quirks, "Прокси на localhost").
# The jar is a FILE: each step is its own process, and a jar that lives only in memory logs in again
# on every call — which looks like a working session right up to the moment a step needs the one it
# thought it had.
JARFILE = os.environ.get("TMB_JAR", os.path.join(W, "stand", "cookies.txt"))
JAR = http.cookiejar.MozillaCookieJar(JARFILE)
try:
JAR.load(ignore_discard=True, ignore_expires=True)
except Exception:
pass
OPENER = urllib.request.build_opener(
urllib.request.ProxyHandler({}), urllib.request.HTTPCookieProcessor(JAR))
def jar_save():
os.makedirs(os.path.dirname(JARFILE), exist_ok=True)
JAR.save(ignore_discard=True, ignore_expires=True)
_seq = [0]
def state_load():
try:
with open(STATE) as f:
return json.load(f)
except FileNotFoundError:
return {}
def state_put(**kw):
s = state_load()
s.update(kw)
os.makedirs(os.path.dirname(STATE), exist_ok=True)
with open(STATE, "w") as f:
json.dump(s, f, indent=2)
return s
def sql(q):
out = subprocess.run([PSQL, DSN, "-At", "-F", "\t", "-c", q],
capture_output=True, text=True)
if out.returncode != 0:
raise SystemExit("psql refused: " + out.stderr.strip())
return [l.split("\t") for l in out.stdout.splitlines()]
def sql1(q, default=None):
rows = sql(q)
return rows[0][0] if rows and rows[0] and rows[0][0] != "" else default
def api(method, path, body=None, ctype=None, label=None, raw=False, headers=None):
"""One request. The request AND the answer are on disk before this returns."""
_seq[0] += 1
n = _seq[0]
url = f"http://{ADDR}{path}"
req = urllib.request.Request(url, data=body, method=method)
req.add_header("X-TM-Client", "coldrun-b-driver")
if ctype:
req.add_header("Content-Type", ctype)
for k, v in (headers or {}).items():
req.add_header(k, v)
t0 = time.time()
try:
r = OPENER.open(req, timeout=300)
code, data, hdrs = r.status, r.read(), dict(r.headers)
except urllib.error.HTTPError as e:
code, data, hdrs = e.code, e.read(), dict(e.headers)
except Exception as e: # connection refused, timeout, …
code, data, hdrs = 0, str(e).encode(), {}
ms = int((time.time() - t0) * 1000)
name = f"{n:03d}-{(label or path.strip('/').replace('/', '_'))[:60]}"
rec = {"n": n, "at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "method": method, "url": url,
"status": code, "ms": ms, "headers": hdrs,
"request_ctype": ctype, "request_bytes": len(body or b"")}
os.makedirs(HTTPDIR, exist_ok=True)
if raw:
with open(os.path.join(HTTPDIR, name + ".bin"), "wb") as f:
f.write(data)
rec["body_file"] = name + ".bin"
rec["body_bytes"] = len(data)
else:
try:
rec["body"] = json.loads(data.decode("utf-8"))
except Exception:
rec["body_text"] = data.decode("utf-8", "replace")[:20000]
with open(os.path.join(HTTPDIR, name + ".json"), "w") as f:
json.dump(rec, f, ensure_ascii=False, indent=2)
with open(os.path.join(W, "evidence", "http-index.jsonl"), "a") as f:
f.write(json.dumps({k: rec[k] for k in ("n", "at", "method", "url", "status", "ms")}) + "\n")
jar_save()
return code, (rec.get("body") if not raw else data), hdrs
def multipart(fields, filefield, filepath):
b = "----tmb" + uuid.uuid4().hex
out = []
for k, v in fields.items():
out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode())
fn = os.path.basename(filepath)
ct = mimetypes.guess_type(fn)[0] or "application/octet-stream"
out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{filefield}\"; filename=\"{fn}\"\r\n"
f"Content-Type: {ct}\r\n\r\n".encode())
out.append(open(filepath, "rb").read())
out.append(f"\r\n--{b}--\r\n".encode())
return b"".join(out), f"multipart/form-data; boundary={b}"
# ---------------------------------------------------------------- the gate
FREEZE_SHA = os.environ.get("FREEZE_SHA", "baa06cef7b2b30b8f7cbe031f3d0fabc03a01a55")
BINARIES = ["tmctl", "tmplatformd", "tmplatformctl", "stub", "zeropipe"]
def stamp(path):
out = subprocess.run(["go", "version", "-m", path], capture_output=True, text=True).stdout
vcs = [l for l in out.splitlines() if "vcs." in l]
rev = next((l.split("vcs.revision=")[1].strip() for l in vcs if "vcs.revision=" in l), None)
mod = next((l.split("vcs.modified=")[1].strip() for l in vcs if "vcs.modified=" in l), None)
return rev, mod, len(vcs), len(out.splitlines())
def step_gate(args):
"""Refuses; never warns. Three conditions on the stamp, not one — a binary built in a linked
worktree carries NO vcs.* lines, and a gate that only forbids modified=true passes it always."""
bad = []
print("=== binaries: the frozen revision, a clean tree, and a stamp that EXISTS ===")
for b in BINARIES:
p = os.path.join(W, "bin", b)
rev, mod, n, total = stamp(p)
print(f" {b:<14} revision={(rev or 'none')[:12]} modified={mod} vcs_lines={n} (control: build lines {total})")
if n == 0:
bad.append(f"{b}: no vcs.* lines — this gate would be vacuous")
if rev != FREEZE_SHA:
bad.append(f"{b}: revision is not the frozen one")
if mod != "false":
bad.append(f"{b}: built from a dirty tree")
print("=== the stand is MINE: a pid on the port, not a 200 from someone else's process ===")
listeners = subprocess.run(["ss", "-ltnp"], capture_output=True, text=True).stdout.splitlines()[1:]
def owner(port):
for l in listeners:
if f":{port} " in l:
return l.split("users:")[-1] if "users:" in l else l
return None
for port, must in ((ADDR.split(":")[1], True),):
o = owner(port)
print(f" port {port}: {o or 'NOTHING LISTENS'}")
if must and not o:
bad.append(f"port {port}: there is no stand")
stub = owner("11434")
phase = args.phase
if phase == "smoke":
print(f" port 11434 (the $0 stub, REQUIRED in this phase): {stub or 'NOTHING LISTENS'}")
if not stub:
bad.append("the smoke has no stub to answer the local provider")
else:
if stub:
bad.append("something LISTENS on the local provider's address during a PAID phase: a "
"configuration that still reached a local model would be served free prose "
"and the run would measure nothing while looking green")
print(f" port 11434: ⛔ {stub}")
else:
print(f" port 11434: nothing listens (control: {len(listeners)} listeners on this host) "
f"— a local model would now fail LOUDLY")
print("=== what will be bought: sha256 of every file that decides it ===")
for name, path, expect in args.sha or []:
if not os.path.exists(path):
print(f" {name}: ABSENT at {path}")
bad.append(f"{name} is absent")
continue
got = subprocess.run(["sha256sum", path], capture_output=True, text=True).stdout.split()[0]
ok = (expect == "" or expect == got)
print(f" {name}: {got}{'' if expect == '' else (' (matches the pre-registration)' if ok else ' ⛔ the pre-registration says ' + expect)}")
if not ok:
bad.append(f"{name}: sha does not match the pre-registration")
print("=== the freeze tree itself ===")
head = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "rev-parse", "HEAD"],
capture_output=True, text=True).stdout.strip()
dirty = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "status", "--porcelain"],
capture_output=True, text=True).stdout.splitlines()
print(f" HEAD {head} dirty lines: {len(dirty)}")
if os.path.exists("/tmp/.git"):
bad.append("/tmp/.git exists: a build here would lose its stamp")
if bad:
for b in bad:
print("" + b)
raise SystemExit("THE GATE REFUSED; nothing was bought")
print("GATE PASSED")
# ---------------------------------------------------------------- the steps
def step_capabilities(args):
code, body, _ = api("GET", "/v0/capabilities", label="capabilities")
print(f"HTTP {code}")
print(json.dumps(body, ensure_ascii=False, indent=2)[:2000])
if code != 200:
# "empty list" and "never answered" are the same shape in output, and this is the pin the
# whole export third of the path hangs on: say which one happened.
print(f"⛔ the capabilities probe did not answer 200 — export_formats is UNKNOWN, not empty")
return
fmts = (body or {}).get("export_formats")
print(f"export_formats = {fmts!r}{'NON-EMPTY: all three export gates are satisfied' if fmts else '⛔ EMPTY: the export doors are not mounted and the last third of the path does not exist'}")
def step_login(args):
code, body, _ = api("POST", "/auth/dev-login", label="dev-login")
print(f"dev-login HTTP {code}")
rows = sql("select id, coalesce(email,'(none)') from users order by created_at desc limit 5")
print(f" users on this stand: {len(sql('select id from users'))} (control) — newest: {rows[0] if rows else 'none'}")
if rows:
state_put(user=rows[0][0])
print(f" remembered user={rows[0][0]}")
def step_grant(args):
out = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "grant",
"--user", args.user, "--usd", args.usd, "--note", args.note],
capture_output=True, text=True)
print(out.stdout.strip() or out.stderr.strip())
bal = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "balance", "--user", args.user],
capture_output=True, text=True)
print(bal.stdout.strip() or bal.stderr.strip())
rows = sql(f"select kind, amount_micro_usd from credit_ledger where user_id='{args.user}' order by id")
total = sum(int(r[1]) for r in rows)
print(f" ledger rows for THIS account: {len(rows)} (control: rows for ALL accounts "
f"{len(sql('select id from credit_ledger'))}), sum {total} µUSD")
def step_intake(args):
body, ct = multipart({"title": args.title, "source_lang": "zh", "target_lang": "ru"}, "file", args.path)
code, b, _ = api("POST", "/v0/books", body=body, ctype=ct, label="intake")
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
if code != 201:
raise SystemExit("the intake refused")
state_put(book=b["id"])
sha = subprocess.run(["sha256sum", args.path], capture_output=True, text=True).stdout.split()[0]
print(f" remembered book={b['id']} source sha256={sha}")
def _await_options(book, tries=20):
for _ in range(tries):
code, b, _ = api("GET", f"/v0/books/{book}/run-options", label="run-options")
if code == 200 and isinstance(b, dict) and "order" in b:
return code, b
time.sleep(3)
return code, b
def step_options(args):
book = state_load()["book"]
code, b = _await_options(book)
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
order = (b or {}).get("order", {})
ch = order.get("chapters_left")
funded = order.get("term_consistency_funded")
units = sql1(f"select count(*) from units u join chapters c on c.id=u.chapter_id where c.book_id='{book}'", "0")
per = sql(f"""select c.number, count(u.id) from chapters c left join units u on u.chapter_id=c.id
where c.book_id='{book}' group by 1 order by 1""")
for n, k in per:
print(f" chapter {n}: units {k}")
print(f" units_total = {units} chapters_left = {ch} term_consistency_funded = {funded}")
bad = []
if args.chapters is not None and str(ch) != str(args.chapters):
bad.append(f"the slice is not the pre-registered one: {ch} chapters, expected {args.chapters}")
if args.units is not None and str(units) != str(args.units):
bad.append(f"units_total is {units}, the pre-registration says {args.units}")
if args.require_funded and funded is not True:
bad.append(f"term_consistency_funded is {funded}: the hold does not carry the book bond and "
f"the run would quietly lose its terminology consolidation")
if bad:
for x in bad:
print("" + x)
raise SystemExit("STOP before paying")
print("OPTIONS ACCEPTED")
def step_start(args):
book = state_load()["book"]
payload = {"stop_for_signing": bool(args.stop_for_signing)}
if args.chapters is not None:
payload["chapters"] = args.chapters
if args.characters is not None:
payload["characters"] = args.characters
hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {}
code, b, _ = api("POST", f"/v0/books/{book}/runs", body=json.dumps(payload).encode(),
ctype="application/json", label="start", headers=hdrs)
print(f"HTTP {code}\nrequest: {json.dumps(payload, ensure_ascii=False)}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
if code in (200, 201, 202) and isinstance(b, dict) and b.get("id"):
state_put(run=b["id"])
print(f" remembered run={b['id']}")
return code, b
def _engine_numbers(book):
"""What the ENGINE's own project database says. Returns (rows, calls, free_replays, µUSD)."""
d = sql1(f"select workdir from books where id='{book}'")
db = os.path.join(d, "project.db") if d else None
if not db or not os.path.exists(db):
return None, db
import sqlite3
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
rows = c.execute("select count(*) from request_log").fetchone()[0]
hits = c.execute("select count(*) from request_log where tm_hit=1").fetchone()[0]
cost = c.execute("select coalesce(sum(cost_usd),0) from request_log").fetchone()[0]
per = c.execute("""select substr(trace_id,-1), count(*), sum(tm_hit=1), round(sum(cost_usd)*1e6)
from request_log group by 1 order by 1""").fetchall()
c.close()
return {"rows": rows, "calls": rows - hits, "free": hits, "micro": round(cost * 1e6),
"per_attempt": per}, db
def step_watch(args):
"""Liveness is read ACTIVELY and on two axes, per ATTEMPT. A growing log is not a result, and a
hung job is indistinguishable from a running one by the completion signal alone."""
s = state_load()
book, run = s.get("book"), s.get("run")
eng, db = _engine_numbers(book)
att = sql(f"select attempt_no, last_seq, coalesce(exit_code,-999), coalesce(spend_micro_usd,0), unit_name "
f"from run_attempts where run_id='{run}' order by attempt_no")
status = sql1(f"select status from runs where id='{run}'", "?")
paused = sql1(f"select coalesce(paused_reason,'') from runs where id='{run}'", "")
events = sql1(f"select count(*) from book_events where book_id='{book}'", "0")
user = s.get("user")
settled = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}' and kind='settlement'", "0")
bal = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}'", "0")
print(f"{time.strftime('%H:%M:%S')} status={status} paused={paused!r} book_events={events}")
for a in att:
unit = a[4]
active = subprocess.run(["systemctl", "--user", "show", unit + ".service", "-p", "ActiveState", "--value"],
capture_output=True, text=True).stdout.strip() or "unknown"
print(f" attempt {a[0]}: last_seq={a[1]} exit={a[2]} spend(cumulative)={a[3]}µUSD unit={active}")
if eng:
print(f" engine: request_log rows={eng['rows']} (calls={eng['calls']}, free replays={eng['free']}) "
f"spend={eng['micro']}µUSD | per attempt {eng['per_attempt']}")
else:
print(f" engine: no project database yet at {db}")
print(f" platform ledger for THIS account: settlements={settled}µUSD balance={bal}µUSD "
f"(control: ledger rows for ALL accounts {sql1('select count(*) from credit_ledger','0')})")
if paused:
raise SystemExit(f"paused_reason={paused} — STOP and ping the orchestrator")
if eng and args.stop_at and eng["micro"] >= args.stop_at:
raise SystemExit(f"spend reached {eng['micro']}µUSD ≥ {args.stop_at}µUSD — STOP and ping the orchestrator")
def step_evidence(args):
"""Takes the copy the resume DESTROYS. exportBank is called again at the start of the next
attempt and overwrites .bank.json atomically; .bank-stop.txt is truncated before it is rewritten,
so it can only be read while the run stands still."""
s = state_load()
book, run = s["book"], s["run"]
d = sql1(f"select workdir from books where id='{book}'")
out = os.path.join(W, "evidence", args.label)
os.makedirs(out, exist_ok=True)
status = sql1(f"select status from runs where id='{run}'", "?")
print(f"run status while the copy is taken: {status}")
if status == "translating":
raise SystemExit("the run is still moving: .bank-stop.txt would be read half-written")
n = 0
for f in ("project.db.bank.json", "project.db.mined-signature.yaml", "project.db.bank-stop.txt",
"project.db.auto-bank.yaml", "project.db.manifest.json", "events.jsonl",
"project.db.mined-delta.yaml", "project.db"):
src = os.path.join(d, f)
if os.path.exists(src):
subprocess.run(["cp", src, out + "/"])
sha = subprocess.run(["sha256sum", src], capture_output=True, text=True).stdout.split()[0]
print(f" {f}: {sha}")
n += 1
print(f" copied {n} artefacts (control: the book directory holds {len(os.listdir(d))} entries)")
code, b, _ = api("GET", f"/v0/books/{book}/bank", label=f"bank-{args.label}")
t = (b or {}).get("terms") or []
print(f" GET bank: HTTP {code}, terms={len(t)}, total={(b or {}).get('total')}, signed={(b or {}).get('signed')}")
rows = sql(f"select count(*), count(*) filter (where status='approved') from bank_terms where book_id='{book}'")
print(f" bank_terms in Postgres: all={rows[0][0]} approved={rows[0][1]}")
def step_sign(args):
book = state_load()["book"]
doc = open(args.doc, "rb").read()
code, b, _ = api("POST", f"/v0/books/{book}/bank/corrections", body=doc,
ctype="application/json", label=f"corrections-{args.label}")
print(f"HTTP {code}\nrequest: {doc.decode('utf-8')[:800]}\n{json.dumps(b, ensure_ascii=False, indent=2)[:2000]}")
return code, b
def step_resume(args):
run = state_load()["run"]
hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {}
code, b, _ = api("POST", f"/v0/runs/{run}/resume", body=b"{}", ctype="application/json",
label=f"resume{args.label}", headers=hdrs)
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
return code, b
def step_export(args):
book = state_load()["book"]
code, b, _ = api("POST", f"/v0/books/{book}/exports", body=json.dumps({"format": args.format}).encode(),
ctype="application/json", label=f"export-{args.format}-order")
print(f"order HTTP {code}: {json.dumps(b, ensure_ascii=False)}")
if code not in (200, 201, 202):
raise SystemExit("the export door refused")
eid = b["id"]
for _ in range(60):
time.sleep(2)
code, b, _ = api("GET", f"/v0/books/{book}/exports/{eid}", label=f"export-{args.format}-status")
st = (b or {}).get("state")
if st == "ready":
break
if st == "failed":
raise SystemExit(f"the export failed: {b}")
print(f"status HTTP {code}: {json.dumps(b, ensure_ascii=False)}")
code, data, _ = api("GET", f"/v0/books/{book}/exports/{eid}/content",
label=f"export-{args.format}-content", raw=True)
os.makedirs(os.path.join(W, "evidence", "file"), exist_ok=True)
p = os.path.join(W, "evidence", "file", f"book.{args.format}")
open(p, "wb").write(data)
sha = subprocess.run(["sha256sum", p], capture_output=True, text=True).stdout.split()[0]
print(f"download HTTP {code} bytes={len(data)} sha256={sha}")
row = sql(f"select id,format,state,complete,size_bytes from exports where id='{eid}'")
print(f" exports row (complete is read from Postgres — the wire deliberately does not carry it): {row}")
def step_probe(args):
"""One ad-hoc request whose EXPECTED outcome was written down before it was sent (§4.8): a door's
answer read after the fact always looks right."""
body = args.body.encode() if args.body else None
hdrs = {}
if args.idempotency_key:
hdrs["Idempotency-Key"] = args.idempotency_key
if args.header:
for h in args.header:
k, _, v = h.partition(":")
hdrs[k.strip()] = v.strip()
code, b, hh = api(args.method, args.path, body=body,
ctype=("application/json" if body else None), label=args.label, headers=hdrs)
verdict = "AS EXPECTED" if (args.expect_status is None or code == args.expect_status) else "⛔ NOT AS EXPECTED"
print(f"[{args.label}] {args.method} {args.path} -> HTTP {code} (expected {args.expect_status}) {verdict}")
txt = json.dumps(b, ensure_ascii=False)[:600] if not isinstance(b, bytes) else f"<{len(b)} bytes>"
print(f" body: {txt}")
if args.expect_contains and args.expect_contains not in txt:
print(f" ⛔ the answer does not contain {args.expect_contains!r}")
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("gate"); g.add_argument("--phase", default="paid"); g.set_defaults(f=step_gate, sha=[])
for name, fn in (("capabilities", step_capabilities), ("login", step_login)):
p = sub.add_parser(name); p.set_defaults(f=fn)
p = sub.add_parser("grant"); p.add_argument("--user", required=True); p.add_argument("--usd", required=True)
p.add_argument("--note", default="cold run B"); p.set_defaults(f=step_grant)
p = sub.add_parser("intake"); p.add_argument("--path", required=True); p.add_argument("--title", required=True)
p.set_defaults(f=step_intake)
p = sub.add_parser("options"); p.add_argument("--chapters", type=int); p.add_argument("--units", type=int)
p.add_argument("--require-funded", action="store_true"); p.set_defaults(f=step_options)
p = sub.add_parser("start"); p.add_argument("--chapters", type=int); p.add_argument("--characters", type=int)
p.add_argument("--stop-for-signing", action="store_true"); p.add_argument("--idempotency-key")
p.set_defaults(f=step_start)
p = sub.add_parser("watch"); p.add_argument("--stop-at", type=int, default=1000000); p.set_defaults(f=step_watch)
p = sub.add_parser("evidence"); p.add_argument("--label", default="evidence"); p.set_defaults(f=step_evidence)
p = sub.add_parser("sign"); p.add_argument("--doc", required=True); p.add_argument("--label", default="sign")
p.set_defaults(f=step_sign)
p = sub.add_parser("resume"); p.add_argument("--label", default=""); p.add_argument("--idempotency-key")
p.set_defaults(f=step_resume)
p = sub.add_parser("export"); p.add_argument("--format", required=True); p.set_defaults(f=step_export)
p = sub.add_parser("probe"); p.add_argument("--method", default="GET"); p.add_argument("--path", required=True)
p.add_argument("--body"); p.add_argument("--label", required=True); p.add_argument("--expect-status", type=int)
p.add_argument("--expect-contains"); p.add_argument("--idempotency-key")
p.add_argument("--header", action="append"); p.set_defaults(f=step_probe)
a = ap.parse_args()
a.f(a)
if __name__ == "__main__":
main()