Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,5 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
import time
|
| 4 |
-
import random
|
| 5 |
-
import shutil
|
| 6 |
-
import zipfile
|
| 7 |
-
import tempfile
|
| 8 |
-
import threading
|
| 9 |
from difflib import unified_diff
|
| 10 |
|
| 11 |
import gradio as gr
|
|
@@ -15,10 +9,6 @@ import rft_flightrecorder as fr
|
|
| 15 |
LOG_PATH = fr.DEFAULT_LOG_PATH
|
| 16 |
|
| 17 |
|
| 18 |
-
# ---------------------------
|
| 19 |
-
# Helpers
|
| 20 |
-
# ---------------------------
|
| 21 |
-
|
| 22 |
def text_diff(before_text: str, after_text: str):
|
| 23 |
a = (before_text or "").splitlines()
|
| 24 |
b = (after_text or "").splitlines()
|
|
@@ -30,31 +20,6 @@ def download_log():
|
|
| 30 |
return LOG_PATH if os.path.exists(LOG_PATH) else None
|
| 31 |
|
| 32 |
|
| 33 |
-
def _filepath(file_obj) -> str:
|
| 34 |
-
"""
|
| 35 |
-
Gradio file inputs can be:
|
| 36 |
-
- str path
|
| 37 |
-
- dict-like with 'path'
|
| 38 |
-
- object with .name or .path
|
| 39 |
-
This normalises to a filesystem path string.
|
| 40 |
-
"""
|
| 41 |
-
if not file_obj:
|
| 42 |
-
return ""
|
| 43 |
-
if isinstance(file_obj, str):
|
| 44 |
-
return file_obj
|
| 45 |
-
if isinstance(file_obj, dict) and file_obj.get("path"):
|
| 46 |
-
return file_obj["path"]
|
| 47 |
-
for attr in ("path", "name"):
|
| 48 |
-
v = getattr(file_obj, attr, None)
|
| 49 |
-
if isinstance(v, str) and v:
|
| 50 |
-
return v
|
| 51 |
-
return ""
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# ---------------------------
|
| 55 |
-
# UI wrappers
|
| 56 |
-
# ---------------------------
|
| 57 |
-
|
| 58 |
def ui_start_session(model_id, run_mode, notes, sign_start, sk_hex):
|
| 59 |
sid, msg = fr.start_session(LOG_PATH, model_id, run_mode, notes, sign_start, sk_hex)
|
| 60 |
# fan-out the session id into all tabs
|
|
@@ -95,8 +60,7 @@ def ui_export(session_id):
|
|
| 95 |
|
| 96 |
def ui_list_sessions():
|
| 97 |
sessions, msg = fr.list_sessions(LOG_PATH)
|
| 98 |
-
|
| 99 |
-
return gr.update(choices=sessions, value=val), msg
|
| 100 |
|
| 101 |
|
| 102 |
def ui_pick_session(sid):
|
|
@@ -109,12 +73,9 @@ def ui_get_event(session_id, ev_hash):
|
|
| 109 |
|
| 110 |
|
| 111 |
def ui_import_bundle(bundle_file, pk_hex, require_sigs, store_into_log):
|
| 112 |
-
|
| 113 |
-
if not bundle_path:
|
| 114 |
-
return "Missing bundle file.", False, "Upload a ZIP bundle first."
|
| 115 |
-
|
| 116 |
status, ok, report, stored_msg = fr.import_bundle_verify(
|
| 117 |
-
bundle_path=
|
| 118 |
pk_hex=pk_hex,
|
| 119 |
require_signatures=require_sigs,
|
| 120 |
store_into_log=store_into_log,
|
|
@@ -124,239 +85,137 @@ def ui_import_bundle(bundle_file, pk_hex, require_sigs, store_into_log):
|
|
| 124 |
return status, ok, report + (("\n\n" + extra) if extra else "")
|
| 125 |
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
#
|
| 129 |
-
#
|
| 130 |
|
| 131 |
-
def
|
| 132 |
-
|
| 133 |
-
log_path = os.path.join(tmpdir, "flightlog.jsonl")
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
run_mode="deterministic",
|
| 167 |
-
)
|
| 168 |
-
if ev is not None:
|
| 169 |
-
ok_write = True
|
| 170 |
-
break
|
| 171 |
-
if "Busy" in (m or ""):
|
| 172 |
-
time.sleep(0.001 * (attempt + 1))
|
| 173 |
-
continue
|
| 174 |
-
errors.append({"thread": tid, "i": i, "msg": m})
|
| 175 |
-
break
|
| 176 |
-
|
| 177 |
-
if not ok_write:
|
| 178 |
-
errors.append({"thread": tid, "i": i, "msg": "Too many retries (lock contention)"})
|
| 179 |
-
|
| 180 |
-
if i % 40 == 0:
|
| 181 |
-
time.sleep(0.001)
|
| 182 |
-
|
| 183 |
-
t1 = threading.Thread(target=worker, args=(1,))
|
| 184 |
-
t2 = threading.Thread(target=worker, args=(2,))
|
| 185 |
-
t0 = time.time()
|
| 186 |
-
t1.start(); t2.start()
|
| 187 |
-
t1.join(); t2.join()
|
| 188 |
-
dt = time.time() - t0
|
| 189 |
-
|
| 190 |
-
anchor, fin_msg = fr.finalise_session(
|
| 191 |
-
log_path=log_path,
|
| 192 |
-
session_id=sid,
|
| 193 |
-
sign_anchor=False,
|
| 194 |
-
sk_hex=sk_hex,
|
| 195 |
-
model_id="self-test",
|
| 196 |
-
run_mode="deterministic",
|
| 197 |
-
)
|
| 198 |
-
if anchor is None:
|
| 199 |
-
return {"ok": False, "error": fin_msg, "tmpdir": tmpdir}
|
| 200 |
-
|
| 201 |
-
status, ok, report = fr.verify_session(log_path, sid, pk_hex="", require_signatures=False)
|
| 202 |
-
|
| 203 |
-
all_events, corrupt = fr.read_jsonl(log_path)
|
| 204 |
-
evs = fr.events_for_session(all_events, sid)
|
| 205 |
-
evs.sort(key=lambda x: int(x.get("seq", 0)))
|
| 206 |
-
|
| 207 |
-
expected = 2 * n_per_thread + 2 # session_start + events + session_end
|
| 208 |
-
counts_ok = (len(evs) == expected)
|
| 209 |
-
seq_ok = all(int(evs[i].get("seq", 0)) == i + 1 for i in range(len(evs)))
|
| 210 |
-
|
| 211 |
-
return {
|
| 212 |
-
"ok": bool(ok and counts_ok and seq_ok and len(errors) == 0 and corrupt == 0),
|
| 213 |
-
"verify_status": status,
|
| 214 |
-
"verify_ok": ok,
|
| 215 |
-
"event_count": len(evs),
|
| 216 |
-
"expected_count": expected,
|
| 217 |
-
"counts_ok": counts_ok,
|
| 218 |
-
"seq_ok": seq_ok,
|
| 219 |
-
"corrupt_lines": corrupt,
|
| 220 |
-
"append_error_count": len(errors),
|
| 221 |
-
"duration_s": round(dt, 4),
|
| 222 |
-
"report_tail": "\n".join(report.splitlines()[-8:]),
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def _tamper_zip_test() -> dict:
|
| 227 |
-
tmpdir = tempfile.mkdtemp(prefix="rft_selftest_tamper_")
|
| 228 |
-
log_path = os.path.join(tmpdir, "flightlog.jsonl")
|
| 229 |
-
|
| 230 |
-
sk_hex, pk_hex = fr.gen_keys()
|
| 231 |
-
|
| 232 |
-
sid, msg = fr.start_session(
|
| 233 |
-
log_path=log_path,
|
| 234 |
-
model_id="self-test",
|
| 235 |
-
run_mode="deterministic",
|
| 236 |
-
notes="tamper zip test (temp log)",
|
| 237 |
-
sign_start=True,
|
| 238 |
sk_hex=sk_hex,
|
| 239 |
)
|
| 240 |
if not sid:
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
session_id=sid,
|
| 247 |
-
event_type=
|
| 248 |
-
payload_text=
|
| 249 |
parent_event_hash="",
|
| 250 |
-
sign_event=
|
| 251 |
sk_hex=sk_hex,
|
| 252 |
-
model_id=
|
| 253 |
-
run_mode=
|
| 254 |
)
|
| 255 |
-
if ev
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
zip_name, m = fr.export_session_bundle(log_path, sid)
|
| 270 |
-
if zip_name is None:
|
| 271 |
-
return {"ok": False, "error": m}
|
| 272 |
-
|
| 273 |
-
# Move bundle into tempdir to keep repo tidy
|
| 274 |
-
zip_full = os.path.join(tmpdir, os.path.basename(zip_name))
|
| 275 |
-
try:
|
| 276 |
-
shutil.move(zip_name, zip_full)
|
| 277 |
-
except Exception:
|
| 278 |
-
zip_full = zip_name
|
| 279 |
-
|
| 280 |
-
# Untampered must PASS
|
| 281 |
-
status1, ok1, report1, _ = fr.import_bundle_verify(
|
| 282 |
-
bundle_path=zip_full,
|
| 283 |
-
pk_hex=pk_hex,
|
| 284 |
-
require_signatures=True,
|
| 285 |
-
store_into_log=False,
|
| 286 |
-
log_path=log_path,
|
| 287 |
)
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
with zipfile.ZipFile(tampered_zip, "w", compression=zipfile.ZIP_DEFLATED) as outz:
|
| 325 |
-
for mname in members:
|
| 326 |
-
if mname == events_member:
|
| 327 |
-
outz.writestr(mname, new_events_bytes)
|
| 328 |
-
else:
|
| 329 |
-
outz.writestr(mname, z.read(mname))
|
| 330 |
-
|
| 331 |
-
status2, ok2, report2, _ = fr.import_bundle_verify(
|
| 332 |
-
bundle_path=tampered_zip,
|
| 333 |
-
pk_hex=pk_hex,
|
| 334 |
-
require_signatures=True,
|
| 335 |
-
store_into_log=False,
|
| 336 |
-
log_path=log_path,
|
| 337 |
)
|
| 338 |
|
| 339 |
-
return {
|
| 340 |
-
"ok": bool(ok1 is True and ok2 is False),
|
| 341 |
-
"original_ok": ok1,
|
| 342 |
-
"original_status": status1,
|
| 343 |
-
"tampered_ok": ok2,
|
| 344 |
-
"tampered_status": status2,
|
| 345 |
-
"original_report_tail": "\n".join(report1.splitlines()[-8:]),
|
| 346 |
-
"tampered_report_head": "\n".join(report2.splitlines()[:12]),
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
def ui_run_selftests(n_events_each: int):
|
| 351 |
-
spam = _two_tab_spam_test(n_per_thread=int(n_events_each))
|
| 352 |
-
tamper = _tamper_zip_test()
|
| 353 |
-
overall = bool(spam.get("ok")) and bool(tamper.get("ok"))
|
| 354 |
-
return ("PASS" if overall else "FAIL"), overall, {"two_tab_spam": spam, "tamper_zip": tamper}
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
# ---------------------------
|
| 358 |
-
# UI
|
| 359 |
-
# ---------------------------
|
| 360 |
|
| 361 |
with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Party Verification") as demo:
|
| 362 |
gr.Markdown(
|
|
@@ -385,11 +244,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 385 |
sid_record = gr.Textbox(label="session_id (Record Event)", lines=1)
|
| 386 |
|
| 387 |
list_btn.click(fn=ui_list_sessions, outputs=[sessions_dd, sessions_msg])
|
| 388 |
-
sessions_dd.change(
|
| 389 |
-
fn=ui_pick_session,
|
| 390 |
-
inputs=[sessions_dd],
|
| 391 |
-
outputs=[sid_start, sid_record, sid_tl, sid_verify, sid_final],
|
| 392 |
-
)
|
| 393 |
|
| 394 |
with gr.Tab("Start Session"):
|
| 395 |
model_id = gr.Textbox(label="Model ID", value="audit-demo")
|
|
@@ -399,6 +254,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 399 |
start_btn = gr.Button("Start New Session")
|
| 400 |
start_status = gr.Textbox(label="Status", lines=2)
|
| 401 |
|
|
|
|
| 402 |
start_btn.click(
|
| 403 |
fn=ui_start_session,
|
| 404 |
inputs=[model_id, run_mode, notes, sign_start, sk],
|
|
@@ -422,10 +278,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 422 |
value="note",
|
| 423 |
label="event_type",
|
| 424 |
)
|
| 425 |
-
parent_hash = gr.Textbox(
|
| 426 |
-
label="parent_event_hash_sha256 (optional). If empty, defaults to previous event.",
|
| 427 |
-
lines=1,
|
| 428 |
-
)
|
| 429 |
payload_text = gr.Textbox(
|
| 430 |
label="payload (JSON or plain text)",
|
| 431 |
lines=10,
|
|
@@ -442,7 +295,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 442 |
outputs=[event_out, append_status],
|
| 443 |
)
|
| 444 |
|
| 445 |
-
flightlog_file = gr.File(label="flightlog.jsonl (download)"
|
| 446 |
gr.Button("Download flightlog.jsonl").click(fn=download_log, outputs=[flightlog_file])
|
| 447 |
|
| 448 |
with gr.Tab("Timeline"):
|
|
@@ -503,7 +356,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 503 |
|
| 504 |
export_btn = gr.Button("Export session bundle (ZIP)")
|
| 505 |
export_status = gr.Textbox(label="Export status", lines=1)
|
| 506 |
-
export_file = gr.File(label="Bundle download"
|
| 507 |
|
| 508 |
export_btn.click(
|
| 509 |
fn=ui_export,
|
|
@@ -512,11 +365,8 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 512 |
)
|
| 513 |
|
| 514 |
with gr.Tab("Import Bundle"):
|
| 515 |
-
bundle = gr.File(label="Upload rft_flight_bundle_*.zip"
|
| 516 |
-
store_into_log = gr.Checkbox(
|
| 517 |
-
label="Store imported events into local flightlog.jsonl (only if PASS)",
|
| 518 |
-
value=False,
|
| 519 |
-
)
|
| 520 |
import_require_sigs = gr.Checkbox(label="Require signatures on every imported event", value=False)
|
| 521 |
import_btn = gr.Button("Verify bundle")
|
| 522 |
import_status = gr.Textbox(label="Result", lines=1)
|
|
@@ -529,20 +379,6 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 529 |
outputs=[import_status, import_ok, import_report],
|
| 530 |
)
|
| 531 |
|
| 532 |
-
with gr.Tab("Self Tests"):
|
| 533 |
-
gr.Markdown(
|
| 534 |
-
"Runs **two brutal checks** against a **temporary log** (does not touch your real `flightlog.jsonl`).\n\n"
|
| 535 |
-
"- **Two-tab spam:** concurrent appends -> verifies seq + hash chain\n"
|
| 536 |
-
"- **Tamper ZIP:** export -> edit bundle -> verify should FAIL"
|
| 537 |
-
)
|
| 538 |
-
n_each = gr.Slider(30, 250, value=120, step=10, label="Events per thread (spam test)")
|
| 539 |
-
run_tests = gr.Button("Run self-tests")
|
| 540 |
-
tests_status = gr.Textbox(label="Overall result", lines=1)
|
| 541 |
-
tests_ok = gr.Checkbox(label="PASS", value=False)
|
| 542 |
-
tests_json = gr.JSON(label="selftest_report.json")
|
| 543 |
-
|
| 544 |
-
run_tests.click(fn=ui_run_selftests, inputs=[n_each], outputs=[tests_status, tests_ok, tests_json])
|
| 545 |
-
|
| 546 |
with gr.Tab("Diagnostics"):
|
| 547 |
diag_btn = gr.Button("Run diagnostics")
|
| 548 |
diag_out = gr.JSON(label="diagnostics.json")
|
|
@@ -555,4 +391,36 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
|
|
| 555 |
diff_out = gr.Textbox(label="Diff", lines=14)
|
| 556 |
diff_btn.click(fn=text_diff, inputs=[before, after], outputs=[diff_out])
|
| 557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
demo.launch()
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from difflib import unified_diff
|
| 4 |
|
| 5 |
import gradio as gr
|
|
|
|
| 9 |
LOG_PATH = fr.DEFAULT_LOG_PATH
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
def text_diff(before_text: str, after_text: str):
|
| 13 |
a = (before_text or "").splitlines()
|
| 14 |
b = (after_text or "").splitlines()
|
|
|
|
| 20 |
return LOG_PATH if os.path.exists(LOG_PATH) else None
|
| 21 |
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
def ui_start_session(model_id, run_mode, notes, sign_start, sk_hex):
|
| 24 |
sid, msg = fr.start_session(LOG_PATH, model_id, run_mode, notes, sign_start, sk_hex)
|
| 25 |
# fan-out the session id into all tabs
|
|
|
|
| 60 |
|
| 61 |
def ui_list_sessions():
|
| 62 |
sessions, msg = fr.list_sessions(LOG_PATH)
|
| 63 |
+
return gr.Dropdown(choices=sessions, value=(sessions[-1] if sessions else None)), msg
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def ui_pick_session(sid):
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
def ui_import_bundle(bundle_file, pk_hex, require_sigs, store_into_log):
|
| 76 |
+
# gr.File returns a path string
|
|
|
|
|
|
|
|
|
|
| 77 |
status, ok, report, stored_msg = fr.import_bundle_verify(
|
| 78 |
+
bundle_path=bundle_file,
|
| 79 |
pk_hex=pk_hex,
|
| 80 |
require_signatures=require_sigs,
|
| 81 |
store_into_log=store_into_log,
|
|
|
|
| 85 |
return status, ok, report + (("\n\n" + extra) if extra else "")
|
| 86 |
|
| 87 |
|
| 88 |
+
# ============================================================
|
| 89 |
+
# Quickstart (1-click) demo
|
| 90 |
+
# ============================================================
|
| 91 |
|
| 92 |
+
def _j(obj) -> str:
|
| 93 |
+
return json.dumps(obj, ensure_ascii=False)
|
|
|
|
| 94 |
|
| 95 |
+
|
| 96 |
+
def ui_quickstart_run(sign_all: bool, current_sk: str, current_pk: str):
|
| 97 |
+
"""
|
| 98 |
+
One-click guided demo:
|
| 99 |
+
- optionally generate + use keys
|
| 100 |
+
- start session
|
| 101 |
+
- append a realistic sequence of events
|
| 102 |
+
- verify
|
| 103 |
+
- finalise + export
|
| 104 |
+
- fill the other tabs automatically
|
| 105 |
+
"""
|
| 106 |
+
status_lines = []
|
| 107 |
+
|
| 108 |
+
# Decide keys
|
| 109 |
+
if sign_all:
|
| 110 |
+
sk_hex, pk_hex = fr.gen_keys()
|
| 111 |
+
status_lines.append("[OK] Generated fresh Ed25519 keypair for this demo run.")
|
| 112 |
+
else:
|
| 113 |
+
sk_hex, pk_hex = (current_sk or ""), (current_pk or "")
|
| 114 |
+
status_lines.append("[OK] Running unsigned demo (hash-chain only).")
|
| 115 |
+
|
| 116 |
+
model_id = "rft-flightrecorder-demo"
|
| 117 |
+
run_mode = "deterministic"
|
| 118 |
+
|
| 119 |
+
# Start
|
| 120 |
+
sid, start_msg = fr.start_session(
|
| 121 |
+
LOG_PATH,
|
| 122 |
+
model_id=model_id,
|
| 123 |
+
run_mode=run_mode,
|
| 124 |
+
notes="Quickstart demo: prompt → tool_call/result → output → memory_write → finalise → export",
|
| 125 |
+
sign_start=bool(sign_all),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
sk_hex=sk_hex,
|
| 127 |
)
|
| 128 |
if not sid:
|
| 129 |
+
status_lines.append(f"[FAIL] start_session: {start_msg}")
|
| 130 |
+
# Keep UI safe: don’t overwrite fields on failure
|
| 131 |
+
return (
|
| 132 |
+
(sk_hex if sign_all else current_sk),
|
| 133 |
+
(pk_hex if sign_all else current_pk),
|
| 134 |
+
"", "", "", "", "", # session ids fanout
|
| 135 |
+
"\n".join(status_lines),
|
| 136 |
+
[], "No timeline.",
|
| 137 |
+
"FAIL", False, "Quickstart failed at start_session.",
|
| 138 |
+
None, "Not finalised.",
|
| 139 |
+
None, "No export.",
|
| 140 |
+
)
|
| 141 |
|
| 142 |
+
status_lines.append(f"[OK] Started session: {sid}")
|
| 143 |
+
|
| 144 |
+
# Append a realistic sequence of events
|
| 145 |
+
demo_events = [
|
| 146 |
+
("prompt", {"text": "Summarise why tamper-evident memory logs matter for agent systems."}),
|
| 147 |
+
("tool_call", {"tool": "search", "input": {"q": "tamper-evident audit log hash chain"}, "id": "call_01"}),
|
| 148 |
+
("tool_result", {"id": "call_01", "ok": True, "items": [{"title": "Hash chaining overview", "source": "demo"}]}),
|
| 149 |
+
("output", {"text": "Tamper-evident logs make history verifiable: edits break the chain and verification fails."}),
|
| 150 |
+
("memory_write", {"key": "policy.audit_mode", "before": "off", "after": "on", "reason": "enable strict audit trail"}),
|
| 151 |
+
("note", {"checkpoint": "demo_complete", "expected_next": "finalise + export"}),
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
for etype, payload in demo_events:
|
| 155 |
+
ev, msg = fr.append_event(
|
| 156 |
+
log_path=LOG_PATH,
|
| 157 |
session_id=sid,
|
| 158 |
+
event_type=etype,
|
| 159 |
+
payload_text=_j(payload),
|
| 160 |
parent_event_hash="",
|
| 161 |
+
sign_event=bool(sign_all),
|
| 162 |
sk_hex=sk_hex,
|
| 163 |
+
model_id=model_id,
|
| 164 |
+
run_mode=run_mode,
|
| 165 |
)
|
| 166 |
+
if not ev:
|
| 167 |
+
status_lines.append(f"[FAIL] append_event({etype}): {msg}")
|
| 168 |
+
break
|
| 169 |
+
status_lines.append(f"[OK] appended {etype} (seq={ev.get('seq')})")
|
| 170 |
+
|
| 171 |
+
# Load timeline now (even if partial)
|
| 172 |
+
tl_rows, tl_msg = fr.session_timeline_rows(LOG_PATH, sid)
|
| 173 |
+
|
| 174 |
+
# Verify (pre-finalise)
|
| 175 |
+
verify_status, verify_ok, verify_report = fr.verify_session(
|
| 176 |
+
LOG_PATH,
|
| 177 |
+
sid,
|
| 178 |
+
pk_hex=(pk_hex if sign_all else ""),
|
| 179 |
+
require_signatures=bool(sign_all),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
)
|
| 181 |
+
status_lines.append(f"[{ 'OK' if verify_ok else 'FAIL' }] verify_session (pre-finalise): {verify_status}")
|
| 182 |
+
|
| 183 |
+
# Finalise + export only if verification ok
|
| 184 |
+
anchor = None
|
| 185 |
+
fin_msg = "Not finalised."
|
| 186 |
+
export_path = None
|
| 187 |
+
export_msg = "No export."
|
| 188 |
+
|
| 189 |
+
if verify_ok:
|
| 190 |
+
anchor, fin_msg = fr.finalise_session(
|
| 191 |
+
LOG_PATH,
|
| 192 |
+
sid,
|
| 193 |
+
sign_anchor=bool(sign_all),
|
| 194 |
+
sk_hex=sk_hex,
|
| 195 |
+
model_id=model_id,
|
| 196 |
+
run_mode=run_mode,
|
| 197 |
+
)
|
| 198 |
+
status_lines.append(f"[OK] finalise_session: {fin_msg}")
|
| 199 |
+
|
| 200 |
+
export_path, export_msg = fr.export_session_bundle(LOG_PATH, sid)
|
| 201 |
+
status_lines.append(f"[OK] export_session_bundle: {export_msg}")
|
| 202 |
+
else:
|
| 203 |
+
status_lines.append("[SKIP] Finalise/export skipped because verification failed.")
|
| 204 |
+
|
| 205 |
+
# Fan-out session id into all tabs + prefill timeline/verify/finalise/export panels
|
| 206 |
+
start_status_msg = f"Quickstart complete. session_id={sid}"
|
| 207 |
+
|
| 208 |
+
return (
|
| 209 |
+
(sk_hex if sign_all else current_sk),
|
| 210 |
+
(pk_hex if sign_all else current_pk),
|
| 211 |
+
sid, sid, sid, sid, sid, # session ids fanout
|
| 212 |
+
start_status_msg + "\n\n" + "\n".join(status_lines),
|
| 213 |
+
tl_rows, tl_msg,
|
| 214 |
+
verify_status, verify_ok, verify_report,
|
| 215 |
+
anchor, fin_msg,
|
| 216 |
+
export_path, export_msg,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
)
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Party Verification") as demo:
|
| 221 |
gr.Markdown(
|
|
|
|
| 244 |
sid_record = gr.Textbox(label="session_id (Record Event)", lines=1)
|
| 245 |
|
| 246 |
list_btn.click(fn=ui_list_sessions, outputs=[sessions_dd, sessions_msg])
|
| 247 |
+
sessions_dd.change(fn=ui_pick_session, inputs=[sessions_dd], outputs=[sid_start, sid_record, sid_tl, sid_verify, sid_final])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
with gr.Tab("Start Session"):
|
| 250 |
model_id = gr.Textbox(label="Model ID", value="audit-demo")
|
|
|
|
| 254 |
start_btn = gr.Button("Start New Session")
|
| 255 |
start_status = gr.Textbox(label="Status", lines=2)
|
| 256 |
|
| 257 |
+
# fan-out session id to all places
|
| 258 |
start_btn.click(
|
| 259 |
fn=ui_start_session,
|
| 260 |
inputs=[model_id, run_mode, notes, sign_start, sk],
|
|
|
|
| 278 |
value="note",
|
| 279 |
label="event_type",
|
| 280 |
)
|
| 281 |
+
parent_hash = gr.Textbox(label="parent_event_hash_sha256 (optional). If empty, defaults to previous event.", lines=1)
|
|
|
|
|
|
|
|
|
|
| 282 |
payload_text = gr.Textbox(
|
| 283 |
label="payload (JSON or plain text)",
|
| 284 |
lines=10,
|
|
|
|
| 295 |
outputs=[event_out, append_status],
|
| 296 |
)
|
| 297 |
|
| 298 |
+
flightlog_file = gr.File(label="flightlog.jsonl (download)")
|
| 299 |
gr.Button("Download flightlog.jsonl").click(fn=download_log, outputs=[flightlog_file])
|
| 300 |
|
| 301 |
with gr.Tab("Timeline"):
|
|
|
|
| 356 |
|
| 357 |
export_btn = gr.Button("Export session bundle (ZIP)")
|
| 358 |
export_status = gr.Textbox(label="Export status", lines=1)
|
| 359 |
+
export_file = gr.File(label="Bundle download")
|
| 360 |
|
| 361 |
export_btn.click(
|
| 362 |
fn=ui_export,
|
|
|
|
| 365 |
)
|
| 366 |
|
| 367 |
with gr.Tab("Import Bundle"):
|
| 368 |
+
bundle = gr.File(label="Upload rft_flight_bundle_*.zip")
|
| 369 |
+
store_into_log = gr.Checkbox(label="Store imported events into local flightlog.jsonl (only if PASS)", value=False)
|
|
|
|
|
|
|
|
|
|
| 370 |
import_require_sigs = gr.Checkbox(label="Require signatures on every imported event", value=False)
|
| 371 |
import_btn = gr.Button("Verify bundle")
|
| 372 |
import_status = gr.Textbox(label="Result", lines=1)
|
|
|
|
| 379 |
outputs=[import_status, import_ok, import_report],
|
| 380 |
)
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
with gr.Tab("Diagnostics"):
|
| 383 |
diag_btn = gr.Button("Run diagnostics")
|
| 384 |
diag_out = gr.JSON(label="diagnostics.json")
|
|
|
|
| 391 |
diff_out = gr.Textbox(label="Diff", lines=14)
|
| 392 |
diff_btn.click(fn=text_diff, inputs=[before, after], outputs=[diff_out])
|
| 393 |
|
| 394 |
+
# NEW: Quickstart (1-click)
|
| 395 |
+
with gr.Tab("Quickstart (1-click)"):
|
| 396 |
+
gr.Markdown(
|
| 397 |
+
"## Quickstart (1-click)\n"
|
| 398 |
+
"If you just want to see the full workflow **without guessing what to click**, use this.\n\n"
|
| 399 |
+
"**This will:** start a session → append a realistic event sequence → verify → finalise → export a ZIP proof bundle → "
|
| 400 |
+
"and it will auto-fill the other tabs with the `session_id`, timeline, verification report, anchor, and export file."
|
| 401 |
+
)
|
| 402 |
+
quick_sign = gr.Checkbox(label="Sign everything (Ed25519) + generate fresh keys for this run", value=False)
|
| 403 |
+
quick_run = gr.Button("Run Quickstart Demo Now")
|
| 404 |
+
quick_status = gr.Textbox(label="Quickstart status", lines=14)
|
| 405 |
+
|
| 406 |
+
# Wire Quickstart button (updates existing components too)
|
| 407 |
+
quick_run.click(
|
| 408 |
+
fn=ui_quickstart_run,
|
| 409 |
+
inputs=[quick_sign, sk, pk],
|
| 410 |
+
outputs=[
|
| 411 |
+
sk,
|
| 412 |
+
pk,
|
| 413 |
+
sid_start, sid_record, sid_tl, sid_verify, sid_final,
|
| 414 |
+
start_status,
|
| 415 |
+
tl, tl_status,
|
| 416 |
+
verify_msg, verify_ok, verify_report,
|
| 417 |
+
anchor_out, fin_status,
|
| 418 |
+
export_file, export_status,
|
| 419 |
+
],
|
| 420 |
+
)
|
| 421 |
+
# Also show the status in this tab by mirroring Start Session status
|
| 422 |
+
# (no extra logic; we just display whatever Start Session status was set to)
|
| 423 |
+
# Users see it immediately without tab switching.
|
| 424 |
+
gr.Markdown("Tip: after running, open **Timeline**, **Verify Session**, and **Finalise + Export** — everything is already populated.")
|
| 425 |
+
|
| 426 |
demo.launch()
|