RFTSystems commited on
Commit
361d72c
·
verified ·
1 Parent(s): b1c76c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -292
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
- val = sessions[-1] if sessions else None
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
- bundle_path = _filepath(bundle_file)
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=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
- # Self-tests (do NOT touch main LOG_PATH)
129
- # ---------------------------
130
 
131
- def _two_tab_spam_test(n_per_thread: int = 120) -> dict:
132
- tmpdir = tempfile.mkdtemp(prefix="rft_selftest_spam_")
133
- log_path = os.path.join(tmpdir, "flightlog.jsonl")
134
 
135
- sk_hex, _pk_hex = fr.gen_keys()
136
- sid, msg = fr.start_session(
137
- log_path=log_path,
138
- model_id="self-test",
139
- run_mode="deterministic",
140
- notes="two-tab spam test (temp log)",
141
- sign_start=False,
142
- sk_hex=sk_hex,
143
- )
144
- if not sid:
145
- return {"ok": False, "error": msg, "tmpdir": tmpdir}
146
-
147
- errors = []
148
-
149
- def worker(tid: int):
150
- for i in range(n_per_thread):
151
- payload = {"thread": tid, "i": i, "rand": random.random()}
152
- payload_text = json.dumps(payload, ensure_ascii=False)
153
-
154
- # Retry on lock contention
155
- ok_write = False
156
- for attempt in range(60):
157
- ev, m = fr.append_event(
158
- log_path=log_path,
159
- session_id=sid,
160
- event_type="note",
161
- payload_text=payload_text,
162
- parent_event_hash="",
163
- sign_event=False,
164
- sk_hex=sk_hex,
165
- model_id="self-test",
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
- return {"ok": False, "error": msg}
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- for i in range(5):
244
- ev, m = fr.append_event(
245
- log_path=log_path,
 
 
 
 
 
 
 
 
 
 
 
 
246
  session_id=sid,
247
- event_type="note",
248
- payload_text=json.dumps({"i": i, "msg": "hello"}, ensure_ascii=False),
249
  parent_event_hash="",
250
- sign_event=True,
251
  sk_hex=sk_hex,
252
- model_id="self-test",
253
- run_mode="deterministic",
254
  )
255
- if ev is None:
256
- return {"ok": False, "error": m}
257
-
258
- anchor, m = fr.finalise_session(
259
- log_path=log_path,
260
- session_id=sid,
261
- sign_anchor=True,
262
- sk_hex=sk_hex,
263
- model_id="self-test",
264
- run_mode="deterministic",
265
- )
266
- if anchor is None:
267
- return {"ok": False, "error": m}
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
- # Tamper: edit seq=3 payload inside zip; verification must FAIL
290
- tampered_zip = os.path.join(tmpdir, "tampered_" + os.path.basename(zip_full))
291
- with zipfile.ZipFile(zip_full, "r") as z:
292
- members = z.namelist()
293
- events_member = next((m for m in members if m.endswith("_events.jsonl")), None)
294
- if not events_member:
295
- return {"ok": False, "error": "No _events.jsonl in bundle."}
296
-
297
- lines = z.read(events_member).decode("utf-8", errors="replace").splitlines()
298
- new_lines = []
299
- changed = False
300
-
301
- for ln in lines:
302
- try:
303
- obj = json.loads(ln)
304
- except Exception:
305
- new_lines.append(ln)
306
- continue
307
-
308
- if obj.get("spec") == fr.EVENT_SPEC and int(obj.get("seq", 0)) == 3 and not changed:
309
- payload = obj.get("payload")
310
- if not isinstance(payload, dict):
311
- payload = {}
312
- payload["msg"] = "hacked"
313
- obj["payload"] = payload
314
- new_lines.append(json.dumps(obj, ensure_ascii=False))
315
- changed = True
316
- else:
317
- new_lines.append(ln)
318
-
319
- if not changed:
320
- return {"ok": False, "error": "Could not locate seq=3 to tamper."}
321
-
322
- new_events_bytes = ("\n".join(new_lines) + "\n").encode("utf-8")
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)", type="filepath")
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", type="filepath")
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", type="filepath")
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()