File size: 21,642 Bytes
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abed2f7
 
 
d191420
 
 
f75eb4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
b5cfae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f75eb4e
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f75eb4e
d191420
 
 
 
 
 
 
 
 
 
 
 
f75eb4e
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f75eb4e
d191420
 
 
 
 
 
f75eb4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f75eb4e
d191420
e65e174
d191420
f75eb4e
d191420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b8b1e73
d191420
 
b8b1e73
d191420
 
abed2f7
 
 
 
 
f75eb4e
 
abed2f7
 
d191420
 
abed2f7
 
24c5d77
d191420
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
"""
FALCONS.AI "Verify Anything" — Hugging Face Space (Gradio transport)

Refactor of the FastAPI Space to the `gradio` SDK. Look and behaviour are
preserved: the product's dark theme tokens and stylesheet, the sample cards,
the drop grid, the Hub-repo form, a cancel control, and the verdict stack
(five newest, each with the engine's receipt). The verification logic is
NOT here — it lives, byte-identical to the FastAPI original, in
`engine_wrapper.py`, and still invokes the pinned, unmodified
`engine/verify_attestation.py` as a subprocess.

Standing rules (BUILD SPEC section 1), how each survives the transport:
  1. Engine as subprocess only         -> engine_wrapper._run_engine_sync
  2. No model-parsing code             -> none here; JSON metadata only
  3. Nothing retained                  -> per-request temp dirs deleted in
                                          `finally`; Gradio's own upload copy
                                          is deleted in the same `finally`;
                                          analytics off; Gradio launches
                                          uvicorn at log_level="warning" (no
                                          access log); nothing logged.
  4. Missing capability != tampering   -> engine_wrapper (unchanged)
  5. Weights never re-served           -> the verdict stack shows text only;
                                          no file output component exists.

# Verify a Model Surgeon package

Drop in a signed package and this will tell you whether it is intact, what it
contains, and where it came from. **Free, permanently. No account, no sign-up,
nothing stored.**

## What it checks

- **Integrity** — whether a single byte has changed since the package was signed.
- **Lineage** — 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.
- **Scope** — attestations name precisely the files they cover, and this reports
  exactly that. No more, no less.

## What it does not claim

Verification proves that **this file is the file that was signed, and records
what was done to it.** 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.

## Why it is free

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.

The same verifier is open source under Apache-2.0, and **a copy ships inside
every package** — so verification works offline, in an air-gapped environment,
and continues to work whether or not we are still here.

- **Source (CLI + spec):** https://github.com/Falcons-ai/surgeon-verify
- **Where the packages come from:** https://surgeon.falcons.ai/?utm_source=huggingface

## One result worth knowing

During launch testing, this verifier was handed a package signed by a build
**twenty-five releases earlier**. 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 **named the exact file**.

That is the whole point of the thing.
                                          
"""

import base64
import html
import json
import os
import shutil
import tempfile
import threading
import zipfile
from pathlib import Path

import gradio as gr

# ZeroGPU runtime (RealFalconsAI/verifier runs on the free ZeroGPU tier — the
# CPU tier is not selectable for this Space): the runtime kills any app that
# declares no @spaces.GPU function at startup ("No @spaces.GPU function
# detected"). The verifier needs no GPU, so the declared function is a no-op
# that no control ever calls — every verification runs on the CPU path below
# and consumes no GPU quota. Guarded so `python app.py` runs anywhere.
try:
    import spaces as _spaces
except Exception:  # not on Spaces, or the package is absent
    _spaces = None

if _spaces is not None:
    @_spaces.GPU(duration=1)
    def _zerogpu_placeholder():
        """Declared for the ZeroGPU runtime only. Never invoked."""
        return None

from engine_wrapper import (
    BUILD_STAMP, LOCAL_CMD, MANIFEST, MAX_UPLOAD_BYTES, MAX_UPLOAD_MB,
    MAX_REPO_MB, NORMALIZED_NOTE, NOT_A_PACKAGE_COPY, ROOT, SAMPLES_DIR,
    _REPO_ID_RE, _normalize_container, _resolve_and_verify_repo_sync,
    _result, _run_engine_sync, _zip_declared_size,
)

MAX_CONCURRENCY = 4
STACK_MAX = 5

print(f"falcons-verify build {BUILD_STAMP}")

# ------------------------------------------------------------- concurrency
# The FastAPI version counted in-flight requests under an asyncio lock and
# answered 429 when full. Gradio runs handlers in worker threads, so the same
# rule is a non-blocking semaphore: full -> the same "at capacity" verdict.

_slots = threading.Semaphore(MAX_CONCURRENCY)


def _busy_result():
    return _result(
        "error", None, "",
        detail=("The verifier is at capacity right now — give it a few seconds and "
                "try again. " + LOCAL_CMD),
    )


# ---------------------------------------------------------------- verdicts
# pushVerdict() from static/app.js, ported line for line. Same classes, same
# headings, same rules (SCAN F3: a TAMPERED card shows the engine's ✖ line and
# its signed/actual digest pair — never the reassuring signature line).

COPY = {
    "not_a_package": NOT_A_PACKAGE_COPY,
    "repo_not_attested": (
        "This repo doesn't carry a Surgeon attestation, so there's nothing to "
        "verify against — that's a statement about provenance, not quality. "
        "Repos published through Model Surgeon's attested push verify here."
    ),
}


def _e(s):
    return html.escape(str(s), quote=True)


def _first_line_matching(text, needle_lower):
    for ln in (text or "").split("\n"):
        if needle_lower in ln.lower():
            return ln.strip()
    return None


def _first_non_empty(text):
    for ln in (text or "").split("\n"):
        if ln.strip():
            return ln.strip()
    return None


def render_card(res, source_label):
    v = res.get("verdict")
    if v == "verified":
        cls, head = "v-verified", "✔ VERIFIED"
    elif v == "tampered":
        cls, head = "v-tampered", "✘ TAMPERED"
    elif v == "not_a_package":
        cls, head = "v-amber", "NOT A SURGEON PACKAGE"
    elif v == "repo_not_attested":
        cls, head = "v-amber", "REPO NOT ATTESTED"
    else:
        cls, head = "v-error", "LIMIT / ERROR"
    parts = [f'<article class="verdict-card {cls}"><h3 class="vhead">{_e(head)}</h3>']
    if source_label:
        parts.append(f'<p class="vsub">{_e(source_label)}</p>')

    stdout = res.get("stdout") or ""
    if v == "verified":
        if res.get("keyid"):
            parts.append(
                '<p class="vline"><span class="keyid"><span>'
                + _e(res["keyid"])
                + '</span></span><span class="kcaption">pin this to know who signed</span></p>'
            )
        fc = res.get("files_checked")
        if fc is not None:
            parts.append(f'<p class="vline">{_e(fc)} files checked</p>')
        att = _first_line_matching(stdout, "attestation")
        if att:
            parts.append(f'<p class="vline">{_e(att)}</p>')

    if v == "tampered":
        fail = [ln.strip() for ln in stdout.split("\n")
                if ln.lstrip().startswith(("✖", "signed ", "actual "))]
        if not fail:
            fail = [_first_non_empty(stdout)]
        for ln in fail:
            if ln:
                parts.append(f'<p class="vline vfail">{_e(ln)}</p>')

    detail = res.get("detail") or COPY.get(v, "")
    if detail:
        parts.append(f'<p class="vdetail">{_e(detail)}</p>')

    if stdout.strip():
        parts.append(
            '<details class="receipt"><summary>show the receipt</summary><pre>'
            + _e(stdout) + "</pre></details>"
        )
    parts.append("</article>")
    return "".join(parts)


def render_stack(stack):
    if not stack:
        return '<div id="verdictStack"></div>'
    return '<div id="verdictStack">' + "".join(stack) + "</div>"


def _push(stack, res, source_label):
    stack = [render_card(res, source_label)] + list(stack or [])
    return stack[:STACK_MAX]


# ---------------------------------------------------------------- handlers
# Each returns (new_stack_state, rendered_html). Every path releases its slot
# and deletes its temp dir in `finally` (SPEC 1.3).

def _samples():
    return json.loads(MANIFEST.read_text()) if MANIFEST.exists() else []


def verify_sample(sample_id, stack, progress=gr.Progress()):
    if not _slots.acquire(blocking=False):
        s = _push(stack, _busy_result(), sample_id)
        return s, render_stack(s)
    try:
        progress(0, desc="recomputing digests…")
        entry = next((e for e in _samples() if e.get("id") == sample_id), None)
        if entry is None:
            res = _result("error", None, "", detail="Unknown sample id.")
        else:
            sample_path = SAMPLES_DIR / Path(entry["filename"]).name
            if not sample_path.exists():
                res = _result("error", None, "", detail=(
                    "This sample package hasn't been installed on the Space yet. "
                    + LOCAL_CMD))
            else:
                # Server-side copy only — the file never round-trips through
                # the browser.
                res = _run_engine_sync(sample_path)
        s = _push(stack, res, entry["label"] if entry else sample_id)
        return s, render_stack(s)
    finally:
        _slots.release()


def verify_upload(file_path, stack, progress=gr.Progress()):
    if not file_path:
        return stack, render_stack(stack)
    label = Path(file_path).name
    if not _slots.acquire(blocking=False):
        s = _push(stack, _busy_result(), label)
        return s, render_stack(s)
    tmp = tempfile.mkdtemp(prefix="verify-")
    try:
        progress(0, desc="recomputing digests…")
        # Gradio has already streamed the upload to its cache (bounded by
        # launch(max_file_size=…)); it is moved into our per-request dir so
        # exactly one deletion covers everything.
        target = Path(tmp) / "upload.bin"
        shutil.move(file_path, target)
        received = target.stat().st_size
        if received > MAX_UPLOAD_BYTES:
            res = _result("error", None, "", detail=(
                "That file is over this verifier's " + str(MAX_UPLOAD_MB)
                + " MB upload cap. " + LOCAL_CMD))
        elif received == 0:
            res = _result("error", None, "", detail="No file was received. " + LOCAL_CMD)
        else:
            res = None
            # Zip-bomb guard: declared uncompressed size, before any extraction.
            if zipfile.is_zipfile(target):
                try:
                    if _zip_declared_size(target) > 2 * MAX_UPLOAD_BYTES:
                        res = _result("error", None, "", detail=(
                            "This archive declares an uncompressed size more than "
                            "twice the " + str(MAX_UPLOAD_MB)
                            + " MB cap, so it won't be opened here. " + LOCAL_CMD))
                except zipfile.BadZipFile:
                    pass  # let the engine speak for itself
            if res is None:
                # Any file type is accepted; the engine's own output routes
                # non-zips and attestation-less zips to the polite
                # not-a-package card. Windows/macOS-made zips get their
                # container repaired first.
                target, normalized = _normalize_container(target, tmp)
                res = _run_engine_sync(target)
                if normalized and isinstance(res, dict):
                    res["detail"] = NORMALIZED_NOTE + (res.get("detail") or "")
        s = _push(stack, res, label)
        return s, render_stack(s)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)          # SPEC 1.3 — nothing retained
        try:
            Path(file_path).unlink(missing_ok=True)      # Gradio's cached copy, if any remains
        except Exception:
            pass
        _slots.release()


def verify_repo(repo_id, stack, progress=gr.Progress()):
    repo_id = (repo_id or "").strip()
    if not repo_id:
        return stack, render_stack(stack)
    if not _slots.acquire(blocking=False):
        s = _push(stack, _busy_result(), repo_id)
        return s, render_stack(s)
    tmp = tempfile.mkdtemp(prefix="verify-repo-")
    try:
        progress(0, desc="fetching from the Hub…")
        if not _REPO_ID_RE.match(repo_id):
            res = _result("error", None, "", detail=(
                "That doesn't look like a Hugging Face repo id — the format is "
                "owner/name. " + LOCAL_CMD))
        else:
            # Wall-clock: the download itself is bounded inside the resolver's
            # hf_hub_download calls; the ✕ cancel button aborts the event.
            res = _resolve_and_verify_repo_sync(repo_id, Path(tmp))
        s = _push(stack, res, repo_id)
        return s, render_stack(s)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)          # SPEC 1.3 — nothing retained
        _slots.release()


# ---------------------------------------------------------------- the page

def _data_uri(path, mime):
    return f"data:{mime};base64," + base64.b64encode(Path(path).read_bytes()).decode()


LOGO = _data_uri(ROOT / "static" / "logo.png", "image/png")
CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") + (ROOT / "static" / "gradio.css").read_text(encoding="utf-8")

MASTHEAD = f"""
<header class="masthead">
  <a class="brandmark" href="https://surgeon.falcons.ai" target="_blank" rel="noopener" aria-label="FALCONS.AI">
    <img src="{LOGO}" alt="FALCONS.AI falcon logo" width="72" height="72">
  </a>
  <div class="masthead-text">
    <h1>PROVENANCE VERIFIER</h1>
    <p class="byline">by <span class="brand-name">FALCONS.AI</span></p>
    <p class="sub">free for everyone · no account needed · <span class="brand">Model Surgeon</span></p>
  </div>
</header>"""

FOOTER = """
<h1>Verify a Model Surgeon package</h1>
<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>

<h2>What it checks</h2>
<ul>
  <li><strong>Integrity</strong> — whether a single byte has changed since the package was signed.</li>
  <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>
  <li><strong>Scope</strong> — attestations name precisely the files they cover, and this reports exactly that. No more, no less.</li>
</ul>

<h2>What it does not claim</h2>
<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>

<h2>Why it is free</h2>
<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>
<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>
<ul>
  <li><strong>Source (CLI + spec):</strong> <a href="https://github.com/Falcons-ai/surgeon-verify">https://github.com/Falcons-ai/surgeon-verify</a></li>
  <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>
</ul>

<h2>One result worth knowing</h2>
<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>
<p>That is the whole point of the thing.</p>




<footer class="footer">
  <nav class="footlinks">
    <a href="https://surgeon.falcons.ai/pricing">Pricing</a> ·
    <a href="https://surgeon.falcons.ai/terms">Terms</a> ·
    <a href="https://surgeon.falcons.ai/eu-ai-act">EU AI Act</a> ·
    <a href="https://surgeon.falcons.ai/privacy">Privacy</a> ·
    <a href="https://surgeon.falcons.ai">surgeon.falcons.ai</a> ·
    <a href="https://github.com/Falcons-ai/surgeon-verify">GitHub: surgeon-verify</a>
  </nav>
  <p class="strapline">Signed creation is the product — universal verification is what makes it worth anything. Free, no account, forever.</p>
</footer>"""

# Force the dark theme regardless of the visitor's OS setting (the product is
# dark only, SPEC 3) — Gradio otherwise follows prefers-color-scheme.
FORCE_DARK_JS = """
() => {
  const u = new URL(window.location);
  if (u.searchParams.get('__theme') !== 'dark') {
    u.searchParams.set('__theme', 'dark');
    window.location.replace(u.toString());
  }
}
"""


def build():
    samples = _samples()
    with gr.Blocks(title="Provenance Verifier · FALCONS.AI Model Surgeon",
                   css=CSS, js=FORCE_DARK_JS, analytics_enabled=False,
                   delete_cache=(60, 60),                 # Gradio's cache: sweep every minute, nothing older than a minute
                   theme=gr.themes.Base(font=gr.themes.GoogleFont("IBM Plex Mono"),
                                        font_mono=gr.themes.GoogleFont("IBM Plex Mono"))) as ui:
        stack = gr.State([])
        gr.HTML(MASTHEAD)

        with gr.Row(elem_classes="workbench"):
            with gr.Column(scale=1, min_width=260, elem_classes="samples"):
                gr.HTML('<h2 class="panel-label">SAMPLE PACKAGES</h2>')
                sample_buttons = []
                for entry in samples:
                    b = gr.Button(f"▤  {entry['label']}", elem_classes="sample-card")
                    gr.HTML(f'<p class="blurb">{_e(entry["blurb"])}</p>')
                    sample_buttons.append((b, entry["id"]))
                gr.HTML('<p class="hint">tap a sample to verify it</p>')

            with gr.Column(scale=2, elem_classes="verify"):
                gr.HTML('<h2 class="panel-label">THE GRID</h2>')
                grid = gr.File(label="drop any .zip of your own here — or click to choose a file",
                               file_count="single", type="filepath",
                               elem_id="grid", elem_classes="grid")
                with gr.Row(elem_classes="alt-inputs"):
                    repo = gr.Textbox(show_label=False, placeholder="owner/name — e.g. an attested Surgeon push",
                                      elem_classes="repoinput", scale=3, max_lines=1)
                    repo_btn = gr.Button("VERIFY", elem_classes="repobtn", scale=1)
                    cancel_btn = gr.Button("✕ cancel", elem_classes="cancel", scale=1)

        verdicts = gr.HTML(render_stack([]), elem_classes="verdicts")
        gr.HTML(FOOTER)
        gr.HTML(f'<p class="buildtag">build {_e(BUILD_STAMP)} · upload cap {MAX_UPLOAD_MB} MB · repo cap {MAX_REPO_MB} MB</p>')

        events = []
        for b, sid in sample_buttons:
            events.append(b.click(verify_sample, inputs=[gr.State(sid), stack], outputs=[stack, verdicts],
                                  concurrency_limit=MAX_CONCURRENCY))
        events.append(grid.upload(verify_upload, inputs=[grid, stack], outputs=[stack, verdicts],
                                  concurrency_limit=MAX_CONCURRENCY))
        events.append(repo.submit(verify_repo, inputs=[repo, stack], outputs=[stack, verdicts],
                                  concurrency_limit=MAX_CONCURRENCY))
        events.append(repo_btn.click(verify_repo, inputs=[repo, stack], outputs=[stack, verdicts],
                                     concurrency_limit=MAX_CONCURRENCY))
        # Same behaviour as the AbortController in the old front end: the
        # in-flight verification is dropped; nothing is recorded for it.
        cancel_btn.click(None, None, None, cancels=events)
    return ui


ui = build()

if __name__ == "__main__":
    # `sdk: gradio` on Spaces runs this file and owns the port through
    # GRADIO_SERVER_NAME / GRADIO_SERVER_PORT — launch through Gradio, never a
    # hand-rolled server (a second uvicorn fought the runtime for the port).
    # SPEC 1.3 holds: Gradio starts uvicorn at log_level="warning", so no
    # access log is written; this module never logs user input.
    ui.queue(default_concurrency_limit=MAX_CONCURRENCY)
    ui.launch(
        server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
        server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
        max_file_size=f"{MAX_UPLOAD_MB}mb",       # the streamed upload cap (SPEC 4.2)
        show_api=False,
        favicon_path=str(ROOT / "static" / "favicon.png"),
        show_error=False,
        ssr_mode=False,     # experimental Node-side rendering off: nothing here needs it, and a CPU Space shouldn't run a Node sidecar
    )