Spaces:
Sleeping
Sleeping
File size: 23,220 Bytes
6741fc6 | 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 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 | #!/usr/bin/env python3
"""Pre-flight the deployed stack, the way `make audit` pre-flights the source.
`make audit` proves 142 tests against a local PostgreSQL. Nothing proved anything
against the deployment, so every hosted failure so far was found by opening the
page and looking. Two of the worst leave no trace in the API's logs at all,
because in both cases no request ever reaches the API: a UI redirecting every
visitor to a login page, and a `connect-src` baked at build time that still names
the previous API host. Both look exactly like an outage from the outside.
Run it before a demo. It also warms the Space, which sleeps after 48 hours idle,
and the Neon compute behind it, which suspends sooner — so the first document of
the call is not the slow one.
make verify-hosted
Standard library only, deliberately: this checks a deployment, and should not
itself depend on a working install.
## On cleaning up after itself
It does not, and cannot. `audit_log` is append-only, enforced by a database
trigger that raises on `UPDATE` and `DELETE` — including the cascade from
`documents`. That is the product's central claim, not an oversight, so a hosted
check is not given a back door through it.
What replaces deletion is idempotency. The document uploaded here is pinned: the
same bytes every run, so `UNIQUE(file_hash)` means the first run creates one row
and every run afterwards is told `duplicate` and creates nothing. The ledger
gains one permanent, clearly-named record and never grows again — and the second
run onwards turns the clean-up step into a live proof of the idempotency
guarantee, which is a better check than the one it replaces.
`--fresh` makes the bytes unique so the whole pipeline genuinely re-runs. That
adds a permanent row each time, which is why it is not the default.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Final
REPO_ROOT: Final = Path(__file__).resolve().parents[1]
UI_DEFAULT: Final = "https://ledgerlens-jet.vercel.app"
API_DEFAULT: Final = "https://Abdr007-ledgerlens.hf.space"
#: A real degraded scan from the labelled evaluation set — a phone photo of an
#: invoice, committed to the repository, so this runs from a fresh clone.
SAMPLE: Final = REPO_ROOT / "eval" / "testset" / "EV-CE-002-scan.jpg"
SAMPLE_NAME: Final = "hosted-verification.jpg"
# A cold Space wakes in ~30 s, and the Neon compute behind it can take another
# few seconds to resume its first connection.
TIMEOUT_S: Final = 120
#: How long to wait for one document to reach a terminal state.
PIPELINE_DEADLINE_S: Final = 90
HTTP_OK: Final = 200
HTTP_ACCEPTED: Final = 202
HTTP_FOUND: Final = 302
HTTP_FORBIDDEN: Final = 403
HTTP_TOO_MANY_REQUESTS: Final = 429
HIGH_SEVERITIES: Final = frozenset({"HIGH", "CRITICAL"})
#: Spec §2: Ingest -> Route -> Extract -> Validate -> Screen -> Ledger.
PIPELINE_STAGES: Final = ("ingest", "route", "extract", "validate", "screen", "ledger")
class CheckError(Exception):
"""A check that did not hold. The message is what the presenter needs to read."""
def _lower_keys(headers: Any) -> dict[str, str]:
"""Header names, lower-cased.
``dict(response.headers)`` keeps the wire casing and loses the case-insensitive
lookup the underlying ``Message`` provides. Over HTTP/2 the names arrive
lower-cased anyway, so a lower-cased lookup against that dict works — and then
silently stops working over HTTP/1.1, where Vercel sends
``Content-Security-Policy``.
"""
return {str(key).lower(): str(value) for key, value in headers.items()}
def _request(
url: str,
*,
method: str = "GET",
body: bytes | None = None,
headers: dict[str, str] | None = None,
) -> tuple[int, dict[str, str], bytes]:
request = urllib.request.Request(url, data=body, method=method) # noqa: S310
for key, value in (headers or {}).items():
request.add_header(key, value)
try:
with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response: # noqa: S310
return int(response.status), _lower_keys(response.headers), response.read()
except urllib.error.HTTPError as exc:
return int(exc.code), _lower_keys(exc.headers), exc.read()
def _json(url: str, *, origin: str, expect: int = HTTP_OK) -> Any:
status, _, raw = _request(url, headers={"Origin": origin})
if status != expect:
raise CheckError(f"{url} returned HTTP {status}, expected {expect}")
return json.loads(raw)
# --------------------------------------------------------------------------- #
# Checks #
# --------------------------------------------------------------------------- #
def check_ui_is_public(ui: str) -> str:
"""A 302 here means deployment protection is on and visitors hit a login page."""
status, headers, _ = _request(ui)
if status == HTTP_FOUND:
location = headers.get("location", "")
if "vercel.com" in location:
raise CheckError(
"the UI redirects to Vercel login — deployment protection is enabled. "
"Project -> Settings -> Deployment Protection -> disable Vercel Authentication"
)
raise CheckError(f"the UI redirects to {location}")
if status != HTTP_OK:
raise CheckError(f"the UI returned HTTP {status}")
return f"HTTP 200 at {ui}"
def check_csp_points_at_the_api(ui: str, api: str) -> str:
"""The CSP is baked at build time; a stale one blocks every call inside the browser."""
_, headers, _ = _request(ui)
csp = headers.get("content-security-policy", "")
if not csp:
raise CheckError("the UI sends no Content-Security-Policy")
connect = next((part for part in csp.split(";") if part.strip().startswith("connect-src")), "")
host = api.split("://", 1)[-1]
if host.lower() not in connect.lower():
raise CheckError(
f"CSP connect-src does not name {host} — it reads '{connect.strip()}'. "
"NEXT_PUBLIC_API_BASE_URL was not set at BUILD time; redeploy after setting it"
)
return f"connect-src names {host}"
def check_api_health(api: str, origin: str) -> str:
data = _json(f"{api}/health", origin=origin)
if data.get("status") != "ok":
raise CheckError(
f"health is '{data.get('status')}' with database '{data.get('database')}' — "
"the API is up but its database is not; check DATABASE_URL on the Space"
)
if data.get("database") != "up":
raise CheckError(f"database is '{data.get('database')}'")
return (
f"v{data.get('version')} · {data.get('environment')} · db up · "
f"llm {data.get('llm_mode')} · langfuse {data.get('langfuse')}"
)
def check_cors(api: str, ui: str) -> str:
"""The UI may write; an unknown origin may not — asserted on the server.
This deliberately does not test `Access-Control-Allow-Origin`, because on this
host that header is not ours. Hugging Face answers the pre-flight at its edge
and echoes whatever `Origin` it was sent, so an unknown origin is told it is
allowed while `ALLOWED_ORIGINS` says otherwise — the same image refuses
`evil.example` when run locally (AUDIT.md §4c). A check reading that header
would have to either fail forever against a condition nobody can fix, or be
softened until it asserted nothing.
So it checks the thing that is actually enforceable: `OriginGuardMiddleware`
rejects a cross-origin *write* before any handler runs, which no upstream
header rewriting can undo. The pre-flight is still checked from the UI's own
origin, because that one failing would break the product.
"""
status, _, _ = _request(
f"{api}/v1/documents",
method="OPTIONS",
headers={
"Origin": ui,
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type",
},
)
if status != HTTP_OK:
raise CheckError(f"pre-flight from {ui} returned HTTP {status}")
body, content_type = _multipart("cross-origin-probe.jpg", b"\xff\xd8\xff\xe0 probe")
status, _, raw = _request(
f"{api}/v1/documents",
method="POST",
body=body,
headers={"Content-Type": content_type, "Origin": "https://evil.example"},
)
if status != HTTP_FORBIDDEN:
raise CheckError(
f"a write from https://evil.example returned HTTP {status}, not 403 — "
"OriginGuardMiddleware is not in force, and on this host the CORS "
"headers will not stop it either"
)
try:
code = json.loads(raw)["error"]["code"]
except (json.JSONDecodeError, KeyError, TypeError):
code = "?"
if code != "forbidden_origin":
raise CheckError(f"cross-origin write refused with '{code}', not 'forbidden_origin'")
# Reported, never asserted: it is the platform's behaviour, not the app's.
_, headers, _ = _request(
f"{api}/v1/documents",
method="OPTIONS",
headers={"Origin": "https://evil.example", "Access-Control-Request-Method": "POST"},
)
edge = (
"; edge echoes any Origin (platform, see AUDIT §4c)"
if headers.get("access-control-allow-origin")
else ""
)
return f"{ui} may write, evil.example gets 403 forbidden_origin{edge}"
def check_ledger_reconciles(api: str, origin: str) -> str:
"""The KPI cards and the ledger must be counting the same thing.
Both read the same tables through different queries. They drifting apart is
the quiet failure: a dashboard that shows a number nothing else agrees with.
"""
stats = _json(f"{api}/v1/stats", origin=origin)
listing = _json(f"{api}/v1/documents?limit=200", origin=origin)
if listing["total"] != stats["documents_total"]:
raise CheckError(
f"stats says {stats['documents_total']} documents, the ledger says {listing['total']}"
)
counted: dict[str, int] = {}
for item in listing["items"]:
counted[item["status"]] = counted.get(item["status"], 0) + 1
for label, key in (
("DONE", "documents_processed"),
("NEEDS_REVIEW", "documents_needs_review"),
("FAILED", "documents_failed"),
):
if counted.get(label, 0) != stats[key]:
raise CheckError(
f"stats.{key}={stats[key]} but the ledger holds "
f"{counted.get(label, 0)} {label} documents"
)
return (
f"{stats['documents_total']} documents · {stats['documents_processed']} done · "
f"{stats['documents_needs_review']} in review · {stats['anomalies_open']} open flags · "
f"avg {stats['avg_latency_ms']:.0f} ms · p95 {stats['p95_latency_ms']:.0f} ms"
)
def _routing_is_consistent(detail: dict[str, Any]) -> str | None:
"""The thesis, as an assertion: nothing failing verification sits in the ledger.
Returns a description of the violation, or None when the record is consistent.
"""
extraction = detail.get("extraction") or {}
failed = [c["rule"] for c in extraction.get("checks", []) if not c["passed"]]
high = [a for a in detail.get("anomalies", []) if a["severity"] in HIGH_SEVERITIES]
status = detail["status"]
if status == "DONE" and (failed or high):
return (
f"{detail['filename']} is DONE but has "
f"{len(failed)} failed check(s) {failed} and {len(high)} high-severity flag(s)"
)
if status == "NEEDS_REVIEW" and not failed and not high:
return f"{detail['filename']} is in review with nothing failing — no reason to be there"
return None
def check_review_routing(api: str, origin: str) -> str:
"""Sweep every document in the live ledger against the product's own claim."""
listing = _json(f"{api}/v1/documents?limit=200", origin=origin)
violations: list[str] = []
reviewed = 0
for item in listing["items"]:
detail = _json(f"{api}/v1/documents/{item['id']}", origin=origin)
if detail["status"] not in {"DONE", "NEEDS_REVIEW"}:
continue
reviewed += 1
if problem := _routing_is_consistent(detail):
violations.append(problem)
if violations:
raise CheckError(f"{len(violations)} document(s) misrouted: {violations[0]}")
return f"{reviewed} documents, every one routed by its own verification result"
def check_review_queue_is_explainable(api: str, origin: str) -> str:
"""Every flag carries a severity, a score and a reason a human can act on."""
anomalies = _json(f"{api}/v1/anomalies?limit=100", origin=origin)
if not anomalies:
raise CheckError("the review queue is empty — nothing to demonstrate")
for anomaly in anomalies:
if not anomaly.get("reason"):
raise CheckError(f"anomaly {anomaly['id']} carries no reason")
if not anomaly.get("evidence"):
raise CheckError(f"anomaly {anomaly['id']} carries no evidence")
if anomaly.get("severity") not in {"LOW", "MEDIUM", "HIGH", "CRITICAL"}:
raise CheckError(f"anomaly {anomaly['id']} has severity '{anomaly.get('severity')}'")
kinds = sorted({a["anomaly_type"] for a in anomalies})
return f"{len(anomalies)} flag(s), all with severity + evidence + reason · {kinds}"
def _multipart(name: str, payload: bytes) -> tuple[bytes, str]:
boundary = "----ledgerlensverify0000"
body = (
(
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="file"; filename="{name}"\r\n'
"Content-Type: image/jpeg\r\n\r\n"
).encode()
+ payload
+ f"\r\n--{boundary}--\r\n".encode()
)
return body, f"multipart/form-data; boundary={boundary}"
def _await_terminal(api: str, origin: str, document_id: str) -> dict[str, Any]:
deadline = time.monotonic() + PIPELINE_DEADLINE_S
state: dict[str, Any] = {}
while time.monotonic() < deadline:
state = _json(f"{api}/v1/documents/{document_id}/status", origin=origin)
if state.get("is_terminal"):
return state
time.sleep(1)
raise CheckError(
f"the document was still '{state.get('status')}' after {PIPELINE_DEADLINE_S}s "
f"(progress {state.get('progress')})"
)
def check_round_trip(api: str, origin: str, *, fresh: bool) -> str:
"""Upload a real document and verify what came back out.
On the first run this drives the whole pipeline. Afterwards the same bytes
are recognised and no work is repeated — which is the guarantee, so it is
asserted rather than worked around. See the module docstring.
"""
if not SAMPLE.exists():
raise CheckError(f"{SAMPLE} is missing from the checkout")
payload = SAMPLE.read_bytes()
name = SAMPLE_NAME
if fresh:
# Trailing bytes after the JPEG end-of-image marker change the SHA-256
# without changing what any decoder reads.
marker = str(time.time_ns()).encode()
payload = payload + b"\xff\xfe" + len(marker).to_bytes(2, "big") + marker
name = f"hosted-verification-{time.time_ns()}.jpg"
body, content_type = _multipart(name, payload)
status, _, raw = _request(
f"{api}/v1/documents",
method="POST",
body=body,
headers={"Content-Type": content_type, "Origin": origin},
)
if status != HTTP_ACCEPTED:
raise CheckError(f"upload returned HTTP {status}: {raw[:300].decode('utf-8', 'replace')}")
upload = json.loads(raw)
state = _await_terminal(api, origin, upload["document_id"])
detail = _json(f"{api}/v1/documents/{upload['document_id']}", origin=origin)
if problem := _routing_is_consistent(detail):
raise CheckError(problem)
seen = {entry["stage"] for entry in detail.get("audit", []) if entry.get("stage")}
missing = [stage for stage in PIPELINE_STAGES if stage not in seen]
if missing:
raise CheckError(f"the audit trail is missing stage(s) {missing} — it should span all six")
extraction = detail.get("extraction") or {}
checks = extraction.get("checks", [])
if not checks:
raise CheckError("no validation report was stored — nothing verified this document")
passed = sum(1 for c in checks if c["passed"])
origin_word = "deduplicated" if upload["duplicate"] else "processed"
return (
f"{origin_word} · {detail['status']} · lane {detail.get('lane')} · "
f"{passed}/{len(checks)} checks passed · {len(seen)}/6 stages audited · "
f"{state.get('latency_ms')} ms"
)
def check_idempotency(api: str, origin: str) -> str:
"""The same bytes a second time must not create a second record."""
before = _json(f"{api}/v1/stats", origin=origin)["documents_total"]
body, content_type = _multipart(SAMPLE_NAME, SAMPLE.read_bytes())
status, _, raw = _request(
f"{api}/v1/documents",
method="POST",
body=body,
headers={"Content-Type": content_type, "Origin": origin},
)
if status != HTTP_ACCEPTED:
raise CheckError(f"re-upload returned HTTP {status}")
upload = json.loads(raw)
if not upload["duplicate"]:
raise CheckError("identical bytes were not recognised as a duplicate")
after = _json(f"{api}/v1/stats", origin=origin)["documents_total"]
if after != before:
raise CheckError(f"the ledger grew from {before} to {after} on a duplicate upload")
return f"same SHA-256 -> duplicate, ledger still {after} documents"
def check_errors_are_typed(api: str, origin: str) -> str:
"""A rejected upload returns the error envelope, never a stack trace."""
body, content_type = _multipart("payload.exe", b"MZ\x90\x00 this is not a document")
status, _, raw = _request(
f"{api}/v1/documents",
method="POST",
body=body,
headers={"Content-Type": content_type, "Origin": origin},
)
text = raw.decode("utf-8", "replace")
if "Traceback" in text or 'File "' in text:
raise CheckError("a rejected upload leaked a stack trace")
if status == HTTP_ACCEPTED:
raise CheckError("an executable was accepted as a document")
try:
code = json.loads(text)["error"]["code"]
except (json.JSONDecodeError, KeyError, TypeError) as exc:
raise CheckError(f"HTTP {status} was not the error envelope: {text[:160]}") from exc
return f"HTTP {status} {code}, no stack trace"
def check_rate_limit_binds(api: str, origin: str) -> str:
"""The ingestion limit must actually stop someone, not merely be configured.
Production reported `X-RateLimit-Limit: 10` on every upload and never returned
429, because `TRUSTED_PROXY_COUNT` was one less than the host's real proxy
depth: the limiter keyed on the address of whichever edge node answered, so
callers were spread across a handful of buckets and none of them filled. A
check that read the headers would have called that a pass.
Rejected uploads are used deliberately. The limiter decorator runs before the
handler, so a 415 consumes budget while writing nothing — the limit can be
driven to its ceiling without adding a single row to an append-only ledger.
"""
boundary = "----ledgerlensratelimit"
body = (
(
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="file"; filename="rate-limit-probe.exe"\r\n'
"Content-Type: application/octet-stream\r\n\r\n"
).encode()
+ b"MZ\x90\x00 deliberately not a document"
+ f"\r\n--{boundary}--\r\n".encode()
)
headers = {"Content-Type": f"multipart/form-data; boundary={boundary}", "Origin": origin}
limit: str | None = None
attempts = 0
# One more than the ceiling this project configures, with headroom for a
# budget already partly spent by the round-trip checks above.
for attempts in range(1, 26):
status, response_headers, _ = _request(
f"{api}/v1/documents", method="POST", body=body, headers=headers
)
limit = response_headers.get("x-ratelimit-limit", limit)
if status == HTTP_TOO_MANY_REQUESTS:
return f"429 after {attempts} rejected uploads (limit {limit}), 0 ledger writes"
if status == HTTP_ACCEPTED:
raise CheckError("an executable was accepted as a document")
raise CheckError(
f"{attempts} uploads in seconds and none was refused, while the API reported "
f"X-RateLimit-Limit: {limit}. The limit is counted but does not bind — "
"TRUSTED_PROXY_COUNT is almost certainly lower than the host's real proxy "
"depth, so the key is a proxy address rather than the caller. The API logs a "
"`proxy_depth_mismatch` warning naming the value to set."
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ui", default=UI_DEFAULT)
parser.add_argument("--api", default=API_DEFAULT)
parser.add_argument(
"--fresh",
action="store_true",
help="force a full pipeline run with unique bytes (adds a permanent ledger row)",
)
args = parser.parse_args(argv)
ui, api = args.ui.rstrip("/"), args.api.rstrip("/")
checks: list[tuple[str, Any]] = [
("UI is public", lambda: check_ui_is_public(ui)),
("CSP points at the API", lambda: check_csp_points_at_the_api(ui, api)),
("API health", lambda: check_api_health(api, ui)),
("cross-origin writes refused", lambda: check_cors(api, ui)),
("ledger reconciles", lambda: check_ledger_reconciles(api, ui)),
("nothing invalid committed", lambda: check_review_routing(api, ui)),
("review queue explains itself", lambda: check_review_queue_is_explainable(api, ui)),
("upload · extract · validate", lambda: check_round_trip(api, ui, fresh=args.fresh)),
("re-upload is idempotent", lambda: check_idempotency(api, ui)),
("rejections are typed", lambda: check_errors_are_typed(api, ui)),
("rate limit binds", lambda: check_rate_limit_binds(api, ui)),
]
print(f"\n UI {ui}\n API {api}\n")
failures: list[str] = []
for name, run in checks:
started = time.perf_counter()
try:
detail = run()
print(f" PASS {name:30} {detail} [{time.perf_counter() - started:.1f}s]")
except CheckError as exc:
failures.append(f"{name}: {exc}")
print(f" FAIL {name:30} {exc}")
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError) as exc:
failures.append(f"{name}: {exc}")
print(f" FAIL {name:30} unreachable: {exc}")
if failures:
print(f"\n {len(failures)} of {len(checks)} checks failed — do not present\n")
return 1
print(f"\n all {len(checks)} checks passed · the Space is warm · ready to present\n")
return 0
if __name__ == "__main__":
sys.exit(main())
|