RFTSystems commited on
Commit
c5dc8bb
·
verified ·
1 Parent(s): b8c302b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -10
app.py CHANGED
@@ -1,5 +1,11 @@
1
  import os
2
  import json
 
 
 
 
 
 
3
  from difflib import unified_diff
4
 
5
  import gradio as gr
@@ -9,6 +15,10 @@ import rft_flightrecorder as fr
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,6 +30,31 @@ def download_log():
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,7 +95,8 @@ def ui_export(session_id):
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,9 +109,12 @@ def ui_get_event(session_id, ev_hash):
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,6 +124,240 @@ def ui_import_bundle(bundle_file, pk_hex, require_sigs, store_into_log):
85
  return status, ok, report + (("\n\n" + extra) if extra else "")
86
 
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Party Verification") as demo:
89
  gr.Markdown(
90
  "# RFT Agent Flight Recorder — Black Box Trace + Third-Party Verification\n"
@@ -112,7 +385,11 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
112
  sid_record = gr.Textbox(label="session_id (Record Event)", lines=1)
113
 
114
  list_btn.click(fn=ui_list_sessions, outputs=[sessions_dd, sessions_msg])
115
- sessions_dd.change(fn=ui_pick_session, inputs=[sessions_dd], outputs=[sid_start, sid_record, sid_tl, sid_verify, sid_final])
 
 
 
 
116
 
117
  with gr.Tab("Start Session"):
118
  model_id = gr.Textbox(label="Model ID", value="audit-demo")
@@ -122,7 +399,6 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
122
  start_btn = gr.Button("Start New Session")
123
  start_status = gr.Textbox(label="Status", lines=2)
124
 
125
- # fan-out session id to all places
126
  start_btn.click(
127
  fn=ui_start_session,
128
  inputs=[model_id, run_mode, notes, sign_start, sk],
@@ -146,7 +422,10 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
146
  value="note",
147
  label="event_type",
148
  )
149
- parent_hash = gr.Textbox(label="parent_event_hash_sha256 (optional). If empty, defaults to previous event.", lines=1)
 
 
 
150
  payload_text = gr.Textbox(
151
  label="payload (JSON or plain text)",
152
  lines=10,
@@ -163,7 +442,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
163
  outputs=[event_out, append_status],
164
  )
165
 
166
- flightlog_file = gr.File(label="flightlog.jsonl (download)")
167
  gr.Button("Download flightlog.jsonl").click(fn=download_log, outputs=[flightlog_file])
168
 
169
  with gr.Tab("Timeline"):
@@ -224,7 +503,7 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
224
 
225
  export_btn = gr.Button("Export session bundle (ZIP)")
226
  export_status = gr.Textbox(label="Export status", lines=1)
227
- export_file = gr.File(label="Bundle download")
228
 
229
  export_btn.click(
230
  fn=ui_export,
@@ -233,8 +512,11 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
233
  )
234
 
235
  with gr.Tab("Import Bundle"):
236
- bundle = gr.File(label="Upload rft_flight_bundle_*.zip")
237
- store_into_log = gr.Checkbox(label="Store imported events into local flightlog.jsonl (only if PASS)", value=False)
 
 
 
238
  import_require_sigs = gr.Checkbox(label="Require signatures on every imported event", value=False)
239
  import_btn = gr.Button("Verify bundle")
240
  import_status = gr.Textbox(label="Result", lines=1)
@@ -247,6 +529,20 @@ with gr.Blocks(title="RFT Agent Flight Recorder — Black Box Trace + Third-Part
247
  outputs=[import_status, import_ok, import_report],
248
  )
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  with gr.Tab("Diagnostics"):
251
  diag_btn = gr.Button("Run diagnostics")
252
  diag_out = gr.JSON(label="diagnostics.json")
 
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
  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
  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
 
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
 
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
  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(
363
  "# RFT Agent Flight Recorder — Black Box Trace + Third-Party Verification\n"
 
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
  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
  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
  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
 
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
  )
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
  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")