Michael Stattelman commited on
Commit
f75eb4e
·
1 Parent(s): b5cfae6

Add application file

Browse files
Files changed (3) hide show
  1. README.md +4 -4
  2. app.py +81 -8
  3. engine/verify_attestation.py +64 -5
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Provenance Verifier — Live Demo
3
  emoji: ✔
4
  colorFrom: gray
5
  colorTo: green
@@ -10,9 +10,9 @@ pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
- # Provenance Verifier — live demo · FALCONS.AI Model Surgeon
14
 
15
- A public, no-account demo where anyone can watch a Model Surgeon package prove
16
  itself. Tap a sample, drop your own `.zip` onto the grid, or point it at a
17
  Hugging Face repo — the verdict comes straight from the unmodified, audited
18
  `verify_attestation.py` from
@@ -31,7 +31,7 @@ Product: [surgeon.falcons.ai](https://surgeon.falcons.ai) ·
31
  | --- | --- |
32
  | `app.py` | the Gradio page and its three handlers (sample · upload · Hub repo), the cancel control, the five-deep verdict stack with receipts |
33
  | `engine_wrapper.py` | the verification wrapper — **every function byte-identical to the FastAPI original** (`_run_engine_sync`, `_parse_stdout`, `_normalize_container`, `_junk_entry`, `_zip_declared_size`, `_resolve_and_verify_repo_sync`, `_dsse_subject_names`, `_result`) |
34
- | `engine/verify_attestation.py` | the pinned engine, verbatim, unchanged |
35
  | `static/style.css` | the product stylesheet, unchanged; `static/gradio.css` holds the few overrides that hide Gradio's own wrappers |
36
  | `samples/` | SAMPLE_A (genuine) · SAMPLE_B (one byte flipped) |
37
 
 
1
  ---
2
+ title: Provenance Verifier
3
  emoji: ✔
4
  colorFrom: gray
5
  colorTo: green
 
10
  license: apache-2.0
11
  ---
12
 
13
+ # Provenance Verifier · FALCONS.AI Model Surgeon
14
 
15
+ A public, no-account verifier where anyone can watch a Model Surgeon package prove
16
  itself. Tap a sample, drop your own `.zip` onto the grid, or point it at a
17
  Hugging Face repo — the verdict comes straight from the unmodified, audited
18
  `verify_attestation.py` from
 
31
  | --- | --- |
32
  | `app.py` | the Gradio page and its three handlers (sample · upload · Hub repo), the cancel control, the five-deep verdict stack with receipts |
33
  | `engine_wrapper.py` | the verification wrapper — **every function byte-identical to the FastAPI original** (`_run_engine_sync`, `_parse_stdout`, `_normalize_container`, `_junk_entry`, `_zip_declared_size`, `_resolve_and_verify_repo_sync`, `_dsse_subject_names`, `_result`) |
34
+ | `engine/verify_attestation.py` | the pinned engine, verbatim from Model Surgeon `tools/verify_attestation.py` (V7.13, 2026-09-08: names the legacy predicate id when it meets one) |
35
  | `static/style.css` | the product stylesheet, unchanged; `static/gradio.css` holds the few overrides that hide Gradio's own wrappers |
36
  | `samples/` | SAMPLE_A (genuine) · SAMPLE_B (one byte flipped) |
37
 
app.py CHANGED
@@ -21,6 +21,51 @@ Standing rules (BUILD SPEC section 1), how each survives the transport:
21
  4. Missing capability != tampering -> engine_wrapper (unchanged)
22
  5. Weights never re-served -> the verdict stack shows text only;
23
  no file output component exists.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  """
25
 
26
  import base64
@@ -75,7 +120,7 @@ _slots = threading.Semaphore(MAX_CONCURRENCY)
75
  def _busy_result():
76
  return _result(
77
  "error", None, "",
78
- detail=("The demo is at capacity right now — give it a few seconds and "
79
  "try again. " + LOCAL_CMD),
80
  )
81
 
@@ -228,7 +273,7 @@ def verify_upload(file_path, stack, progress=gr.Progress()):
228
  received = target.stat().st_size
229
  if received > MAX_UPLOAD_BYTES:
230
  res = _result("error", None, "", detail=(
231
- "That file is over this demo's " + str(MAX_UPLOAD_MB)
232
  + " MB upload cap. " + LOCAL_CMD))
233
  elif received == 0:
234
  res = _result("error", None, "", detail="No file was received. " + LOCAL_CMD)
@@ -241,7 +286,7 @@ def verify_upload(file_path, stack, progress=gr.Progress()):
241
  res = _result("error", None, "", detail=(
242
  "This archive declares an uncompressed size more than "
243
  "twice the " + str(MAX_UPLOAD_MB)
244
- + " MB cap, so the demo won't open it. " + LOCAL_CMD))
245
  except zipfile.BadZipFile:
246
  pass # let the engine speak for itself
247
  if res is None:
@@ -304,13 +349,41 @@ MASTHEAD = f"""
304
  <img src="{LOGO}" alt="FALCONS.AI falcon logo" width="72" height="72">
305
  </a>
306
  <div class="masthead-text">
307
- <h1>PROVENANCE VERIFIER — LIVE DEMO</h1>
308
  <p class="byline">by <span class="brand-name">FALCONS.AI</span></p>
309
  <p class="sub">free for everyone · no account needed · <span class="brand">Model Surgeon</span></p>
310
  </div>
311
  </header>"""
312
 
313
  FOOTER = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  <footer class="footer">
315
  <nav class="footlinks">
316
  <a href="https://surgeon.falcons.ai/pricing">Pricing</a> ·
@@ -338,11 +411,11 @@ FORCE_DARK_JS = """
338
 
339
  def build():
340
  samples = _samples()
341
- with gr.Blocks(title="Provenance Verifier — live demo · FALCONS.AI Model Surgeon",
342
  css=CSS, js=FORCE_DARK_JS, analytics_enabled=False,
343
  delete_cache=(60, 60), # Gradio's cache: sweep every minute, nothing older than a minute
344
  theme=gr.themes.Base(font=gr.themes.GoogleFont("IBM Plex Mono"),
345
- font_mono=gr.themes.GoogleFont("IBM Plex Mono"))) as demo:
346
  stack = gr.State([])
347
  gr.HTML(MASTHEAD)
348
 
@@ -395,8 +468,8 @@ if __name__ == "__main__":
395
  # hand-rolled server (a second uvicorn fought the runtime for the port).
396
  # SPEC 1.3 holds: Gradio starts uvicorn at log_level="warning", so no
397
  # access log is written; this module never logs user input.
398
- demo.queue(default_concurrency_limit=MAX_CONCURRENCY)
399
- demo.launch(
400
  server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
401
  server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
402
  max_file_size=f"{MAX_UPLOAD_MB}mb", # the streamed upload cap (SPEC 4.2)
 
21
  4. Missing capability != tampering -> engine_wrapper (unchanged)
22
  5. Weights never re-served -> the verdict stack shows text only;
23
  no file output component exists.
24
+
25
+ # Verify a Model Surgeon package
26
+
27
+ Drop in a signed package and this will tell you whether it is intact, what it
28
+ contains, and where it came from. **Free, permanently. No account, no sign-up,
29
+ nothing stored.**
30
+
31
+ ## What it checks
32
+
33
+ - **Integrity** — whether a single byte has changed since the package was signed.
34
+ - **Lineage** — the chain of operations performed on the model, and by whom.
35
+ A model pulled, edited and re-pushed keeps its earlier attestation, so the
36
+ chain holds across multiple hands.
37
+ - **Scope** — attestations name precisely the files they cover, and this reports
38
+ exactly that. No more, no less.
39
+
40
+ ## What it does not claim
41
+
42
+ Verification proves that **this file is the file that was signed, and records
43
+ what was done to it.** It does not establish that a model is original, or that
44
+ it is not derived from an open-weight base. That is a different question and
45
+ this tool does not pretend to answer it.
46
+
47
+ ## Why it is free
48
+
49
+ Because a verification tool you have to buy from the party being verified is
50
+ not verification. The person who needs convincing should never have to trust
51
+ us, hold an account, or pay anything.
52
+
53
+ The same verifier is open source under Apache-2.0, and **a copy ships inside
54
+ every package** — so verification works offline, in an air-gapped environment,
55
+ and continues to work whether or not we are still here.
56
+
57
+ - **Source (CLI + spec):** https://github.com/Falcons-ai/surgeon-verify
58
+ - **Where the packages come from:** https://surgeon.falcons.ai/?utm_source=huggingface
59
+
60
+ ## One result worth knowing
61
+
62
+ During launch testing, this verifier was handed a package signed by a build
63
+ **twenty-five releases earlier**. It honoured the old signature correctly. A
64
+ single byte was then altered — one byte out of roughly fourteen million — and
65
+ the verifier caught the tamper and **named the exact file**.
66
+
67
+ That is the whole point of the thing.
68
+
69
  """
70
 
71
  import base64
 
120
  def _busy_result():
121
  return _result(
122
  "error", None, "",
123
+ detail=("The verifier is at capacity right now — give it a few seconds and "
124
  "try again. " + LOCAL_CMD),
125
  )
126
 
 
273
  received = target.stat().st_size
274
  if received > MAX_UPLOAD_BYTES:
275
  res = _result("error", None, "", detail=(
276
+ "That file is over this verifier's " + str(MAX_UPLOAD_MB)
277
  + " MB upload cap. " + LOCAL_CMD))
278
  elif received == 0:
279
  res = _result("error", None, "", detail="No file was received. " + LOCAL_CMD)
 
286
  res = _result("error", None, "", detail=(
287
  "This archive declares an uncompressed size more than "
288
  "twice the " + str(MAX_UPLOAD_MB)
289
+ + " MB cap, so it won't be opened here. " + LOCAL_CMD))
290
  except zipfile.BadZipFile:
291
  pass # let the engine speak for itself
292
  if res is None:
 
349
  <img src="{LOGO}" alt="FALCONS.AI falcon logo" width="72" height="72">
350
  </a>
351
  <div class="masthead-text">
352
+ <h1>PROVENANCE VERIFIER</h1>
353
  <p class="byline">by <span class="brand-name">FALCONS.AI</span></p>
354
  <p class="sub">free for everyone · no account needed · <span class="brand">Model Surgeon</span></p>
355
  </div>
356
  </header>"""
357
 
358
  FOOTER = """
359
+ <h1>Verify a Model Surgeon package</h1>
360
+ <p>Drop in a signed package and this will tell you whether it is intact, what it contains, and where it came from. <strong>Free, permanently. No account, no sign-up, nothing stored.</strong></p>
361
+
362
+ <h2>What it checks</h2>
363
+ <ul>
364
+ <li><strong>Integrity</strong> — whether a single byte has changed since the package was signed.</li>
365
+ <li><strong>Lineage</strong> — the chain of operations performed on the model, and by whom. A model pulled, edited and re-pushed keeps its earlier attestation, so the chain holds across multiple hands.</li>
366
+ <li><strong>Scope</strong> — attestations name precisely the files they cover, and this reports exactly that. No more, no less.</li>
367
+ </ul>
368
+
369
+ <h2>What it does not claim</h2>
370
+ <p>Verification proves that <strong>this file is the file that was signed, and records what was done to it.</strong> It does not establish that a model is original, or that it is not derived from an open-weight base. That is a different question and this tool does not pretend to answer it.</p>
371
+
372
+ <h2>Why it is free</h2>
373
+ <p>Because a verification tool you have to buy from the party being verified is not verification. The person who needs convincing should never have to trust us, hold an account, or pay anything.</p>
374
+ <p>The same verifier is open source under Apache-2.0, and <strong>a copy ships inside every package</strong> — so verification works offline, in an air-gapped environment, and continues to work whether or not we are still here.</p>
375
+ <ul>
376
+ <li><strong>Source (CLI + spec):</strong> <a href="https://github.com/Falcons-ai/surgeon-verify">https://github.com/Falcons-ai/surgeon-verify</a></li>
377
+ <li><strong>Where the packages come from:</strong> <a href="https://surgeon.falcons.ai/?utm_source=huggingface">https://surgeon.falcons.ai/?utm_source=huggingface</a></li>
378
+ </ul>
379
+
380
+ <h2>One result worth knowing</h2>
381
+ <p>During launch testing, this verifier was handed a package signed by a build <strong>twenty-five releases earlier</strong>. It honoured the old signature correctly. A single byte was then altered — one byte out of roughly fourteen million — and the verifier caught the tamper and <strong>named the exact file</strong>.</p>
382
+ <p>That is the whole point of the thing.</p>
383
+
384
+
385
+
386
+
387
  <footer class="footer">
388
  <nav class="footlinks">
389
  <a href="https://surgeon.falcons.ai/pricing">Pricing</a> ·
 
411
 
412
  def build():
413
  samples = _samples()
414
+ with gr.Blocks(title="Provenance Verifier · FALCONS.AI Model Surgeon",
415
  css=CSS, js=FORCE_DARK_JS, analytics_enabled=False,
416
  delete_cache=(60, 60), # Gradio's cache: sweep every minute, nothing older than a minute
417
  theme=gr.themes.Base(font=gr.themes.GoogleFont("IBM Plex Mono"),
418
+ font_mono=gr.themes.GoogleFont("IBM Plex Mono"))) as ui:
419
  stack = gr.State([])
420
  gr.HTML(MASTHEAD)
421
 
 
468
  # hand-rolled server (a second uvicorn fought the runtime for the port).
469
  # SPEC 1.3 holds: Gradio starts uvicorn at log_level="warning", so no
470
  # access log is written; this module never logs user input.
471
+ ui.queue(default_concurrency_limit=MAX_CONCURRENCY)
472
+ ui.launch(
473
  server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
474
  server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
475
  max_file_size=f"{MAX_UPLOAD_MB}mb", # the streamed upload cap (SPEC 4.2)
engine/verify_attestation.py CHANGED
@@ -1,4 +1,16 @@
1
  #!/usr/bin/env python3
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  FALCONS.AI Model Surgeon — FREE attestation verifier.
4
 
@@ -10,12 +22,13 @@ Checks, in order:
10
  1. the package carries a lineage.intoto.jsonl attestation;
11
  2. every subject artifact's SHA-256 matches the signed digest;
12
  3. the DSSE HMAC signature verifies under the given key
13
- (--key or FALCONSAI_HMAC_KEY; defaults to the public demo key).
14
 
15
  Exit codes: 0 VERIFIED · 1 TAMPERED/INCOMPLETE · 2 not a Surgeon package.
16
  Verification is free by design: signed creation is the paid act, universal
17
  verification is what makes the signature worth anything.
18
  """
 
19
  import argparse
20
  import base64
21
  import hashlib
@@ -43,13 +56,18 @@ def main():
43
  print(f"✖ cannot open {a.package}: {e}")
44
  sys.exit(2)
45
  names = z.namelist()
 
 
 
 
 
46
  prefix = ""
47
  roots = {n.split("/", 1)[0] for n in names if n.strip("/")}
48
  if len(roots) == 1 and all("/" in n for n in names if n.strip("/")):
49
  prefix = next(iter(roots)) + "/" # Explorer-style re-zip: descend
50
 
51
  def _zread(nm):
52
- return z.read(prefix + nm)
53
  try:
54
  att = json.loads(_zread("lineage.intoto.jsonl"))
55
  except KeyError:
@@ -62,6 +80,7 @@ def main():
62
  # key, so verification needs no secret and forgery needs the private
63
  # key. Free verification, restored properly.
64
  env_missing = False
 
65
  sig_entry = (att.get("signatures") or [{}])[0] or {}
66
  scheme = sig_entry.get("scheme") or "hmac"
67
  if scheme == "ed25519" and sig_entry.get("publicKey"):
@@ -91,11 +110,17 @@ def main():
91
  "key could have produced this package")
92
  else:
93
  have = None
 
94
  print("signature : legacy shared-key package; supply --key or "
95
  "FALCONSAI_HMAC_KEY to check it (digests still verified)")
96
- sig_ok = hmac.compare_digest(want, have)
97
  stmt = json.loads(payload)
98
- print(f"attestation : {stmt.get('predicateType')}")
 
 
 
 
 
99
  print(f"tool : {(stmt.get('predicate') or {}).get('tool')}")
100
  all_ok = True
101
  for sub in stmt.get("subject", []):
@@ -114,12 +139,46 @@ def main():
114
  if not ok:
115
  print(f" signed {digest}\n actual {actual}")
116
  print(f"signature : {'✔ valid under this key' if sig_ok else '✖ does NOT verify under this key'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  merges = (stmt.get("predicate") or {}).get("merges") or []
118
  if merges:
119
  print(f"merges : {len(merges)} recorded "
120
  f"({', '.join(sorted({m.get('method','lerp') for m in merges}))})")
121
  verdict = "VERIFIED" if sig_ok and all_ok and stmt.get("subject") \
122
- else "INCOMPLETE" if (env_missing and all_ok) \
123
  else "TAMPERED" if stmt.get("subject") else "INCOMPLETE"
124
  print(f"\nVERDICT: {verdict}")
125
  sys.exit(0 if verdict == "VERIFIED" else 1)
 
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Copyright (c) 2026 FALCONS.AI. Licensed under the Apache License, Version 2.0
4
+ # (the "License"); you may not use this file except in compliance with the
5
+ # License. You may obtain a copy of the License at
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations.
11
+ # This file — the free verifier — is the open-source component of Model
12
+ # Surgeon (T3-a, operator-ruled Apache-2.0, 2026-08-31). The product itself
13
+ # is proprietary; see LICENSE-verifier and NOTICE in the package.
14
  """
15
  FALCONS.AI Model Surgeon — FREE attestation verifier.
16
 
 
22
  1. the package carries a lineage.intoto.jsonl attestation;
23
  2. every subject artifact's SHA-256 matches the signed digest;
24
  3. the DSSE HMAC signature verifies under the given key
25
+ (--key or FALCONSAI_HMAC_KEY; defaults to the public key).
26
 
27
  Exit codes: 0 VERIFIED · 1 TAMPERED/INCOMPLETE · 2 not a Surgeon package.
28
  Verification is free by design: signed creation is the paid act, universal
29
  verification is what makes the signature worth anything.
30
  """
31
+ VERIFIER_VERSION = "2" # D-4 (V6.98): chain-aware
32
  import argparse
33
  import base64
34
  import hashlib
 
56
  print(f"✖ cannot open {a.package}: {e}")
57
  sys.exit(2)
58
  names = z.namelist()
59
+ # v6.51 container fix (UPSTREAM_NOTE): some Windows tools zip with
60
+ # backslash separators — normalize member names so those packages
61
+ # verify instead of reading "not a Surgeon package".
62
+ canon = {n.replace("\\", "/"): n for n in names}
63
+ names = list(canon)
64
  prefix = ""
65
  roots = {n.split("/", 1)[0] for n in names if n.strip("/")}
66
  if len(roots) == 1 and all("/" in n for n in names if n.strip("/")):
67
  prefix = next(iter(roots)) + "/" # Explorer-style re-zip: descend
68
 
69
  def _zread(nm):
70
+ return z.read(canon.get(prefix + nm, prefix + nm))
71
  try:
72
  att = json.loads(_zread("lineage.intoto.jsonl"))
73
  except KeyError:
 
80
  # key, so verification needs no secret and forgery needs the private
81
  # key. Free verification, restored properly.
82
  env_missing = False
83
+ sig_uncheckable = False
84
  sig_entry = (att.get("signatures") or [{}])[0] or {}
85
  scheme = sig_entry.get("scheme") or "hmac"
86
  if scheme == "ed25519" and sig_entry.get("publicKey"):
 
110
  "key could have produced this package")
111
  else:
112
  have = None
113
+ sig_uncheckable = True
114
  print("signature : legacy shared-key package; supply --key or "
115
  "FALCONSAI_HMAC_KEY to check it (digests still verified)")
116
+ sig_ok = have is not None and hmac.compare_digest(want, have)
117
  stmt = json.loads(payload)
118
+ print(f"verifier : {VERIFIER_VERSION}")
119
+ pt = stmt.get("predicateType")
120
+ if pt == "https://falcons.ai/model-surgeon/lineage/v1": # packages written before V7.13: the legacy id (same schema)
121
+ print(f"attestation : https://surgeon.falcons.ai/lineage/v1 (predicate type; this package carries the legacy id {pt})")
122
+ else:
123
+ print(f"attestation : {pt}")
124
  print(f"tool : {(stmt.get('predicate') or {}).get('tool')}")
125
  all_ok = True
126
  for sub in stmt.get("subject", []):
 
139
  if not ok:
140
  print(f" signed {digest}\n actual {actual}")
141
  print(f"signature : {'✔ valid under this key' if sig_ok else '✖ does NOT verify under this key'}")
142
+ # D-4 (V6.98, VERIFIER 2): the lineage CHAIN. A prior attestation, when the
143
+ # package carries one, is checked for payload integrity and — when it was
144
+ # signed by THIS package's key — for signature. A prior that cannot be
145
+ # verified here is reported AMBER; it never makes this package TAMPERED.
146
+ chain = (stmt.get("predicate") or {}).get("chain")
147
+ if chain:
148
+ depth = chain.get("depth")
149
+ pr = chain.get("prior")
150
+ if not pr:
151
+ print(f"chain : depth {depth} · no prior attestation reachable at load")
152
+ else:
153
+ try:
154
+ p_env = pr.get("envelope") or {}
155
+ p_payload = base64.b64decode(p_env.get("payload", ""))
156
+ p_digest = hashlib.sha256(p_payload).hexdigest()
157
+ p_sig = (p_env.get("signatures") or [{}])[0]
158
+ intact = p_digest == pr.get("digest")
159
+ same_key = bool(sig_entry.get("publicKey")) and p_sig.get("keyid") == sig_entry.get("keyid")
160
+ p_ok = None
161
+ if intact and same_key and p_sig.get("scheme") == "ed25519" and p_sig.get("sig"):
162
+ try:
163
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey as _PK
164
+ _PK.from_public_bytes(bytes.fromhex(sig_entry["publicKey"])).verify(
165
+ bytes.fromhex(p_sig["sig"]), p_payload)
166
+ p_ok = True
167
+ except Exception:
168
+ p_ok = False
169
+ state = ("prior VERIFIED (same publisher key)" if p_ok else
170
+ "prior INVALID under this key — AMBER" if p_ok is False else
171
+ "prior intact · signed by another key — not verified here (AMBER)" if intact else
172
+ "prior payload does not match its recorded digest — AMBER")
173
+ print(f"chain : depth {depth} · {state} · prior keyid {str(p_sig.get('keyid') or '?')[:20]} · {len(pr.get('operations') or [])} earlier operation(s)")
174
+ except Exception as e:
175
+ print(f"chain : depth {depth} · prior present but unreadable ({e}) — AMBER")
176
  merges = (stmt.get("predicate") or {}).get("merges") or []
177
  if merges:
178
  print(f"merges : {len(merges)} recorded "
179
  f"({', '.join(sorted({m.get('method','lerp') for m in merges}))})")
180
  verdict = "VERIFIED" if sig_ok and all_ok and stmt.get("subject") \
181
+ else "INCOMPLETE" if ((env_missing or sig_uncheckable) and all_ok) \
182
  else "TAMPERED" if stmt.get("subject") else "INCOMPLETE"
183
  print(f"\nVERDICT: {verdict}")
184
  sys.exit(0 if verdict == "VERIFIED" else 1)