508 lines
26 KiB
Python
Executable file
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()
|