Update app.py
Browse files
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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 77 |
status, ok, report, stored_msg = fr.import_bundle_verify(
|
| 78 |
-
bundle_path=
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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")
|