Evidence export of closed runs and rtut seal (RBT-WP-0009-T04)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 21:35:03 +02:00
parent e021fd979f
commit 6d70f4d97d
12 changed files with 149 additions and 19 deletions

View file

@ -301,7 +301,9 @@ class App:
outcome = r.data.get("outcome")
if outcome:
note = f" - {e(r.data['outcome_note'])}" if r.data.get("outcome_note") else ""
box = f"<p><b>Run finished: {e(outcome)}</b>{note}</p>{self.locked_note(r)}"
box = (f"<p><b>Run finished: {e(outcome)}</b>{note}</p>{self.locked_note(r)}"
f"<p><a href='/run/{i}/{e(rid)}/record.json'>Download the run record (JSON)</a> "
"<span class=note>Check it anywhere with <code>rtut seal &lt;file&gt;</code>.</span></p>")
return f"<div id=close class=step><h2>Close this run</h2>{box}</div>"
states = [v["state"] for v in r.data["steps"].values()]
btns = []
@ -480,6 +482,21 @@ def make_handler(app: App, port_holder: list):
self.send_header("Content-Disposition", f"attachment; filename=\"{safe}\"")
self.end_headers(); self.wfile.write(body)
def _send_record(self, path, filename: str):
"""A closed record, exactly as stored, as a download. Its seal can be checked with `rtut seal <file>`."""
import json
data = json.loads(path.read_text())
if not data.get("outcome"):
return self._send_file(409, b"the record can be downloaded once the run is closed", "text/plain; charset=utf-8", None)
state = R.record_seal_state(data)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Rtut-Seal", state)
self.send_header("Content-Security-Policy", "default-src 'none'; sandbox")
self.send_header("Content-Disposition", f"attachment; filename=\"{filename}\"")
self.end_headers(); self.wfile.write(path.read_bytes())
def _host_ok(self) -> bool:
return (self.headers.get("Host", "").split(":")[0] in ("127.0.0.1", "localhost"))
@ -506,6 +523,10 @@ def make_handler(app: App, port_holder: list):
body = UA.set_page(app, parts[1])
if body:
return self._send(body)
if parts[0] == "run" and len(parts) == 4 and parts[3] == "record.json":
return self._send_record(R.load(app.packs[int(parts[1])], parts[2]).path, f"run-{parts[2]}.json")
if parts[0] == "project-run" and len(parts) == 3 and parts[2] == "record.json":
return self._send_record(PRUN.load(app.registry(), parts[1]).path, f"project-run-{parts[1]}.json")
if parts[0] == "run" and len(parts) == 5 and parts[3] == "artefact":
status, body, ctype, dl = UI2.serve_artefact(app, int(parts[1]), parts[2], parts[4])
want_dl = bool(parse_qs(urlparse(self.path).query).get("download"))