Spaces:
Running
Running
chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)
Browse filesAutomated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, szl_spaces_proxy.py, szl_spaces_surface.py
Deleted (gone from the repo + Dockerfile COPY set): (none)
Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.
- Dockerfile +4 -0
- szl_spaces_proxy.py +337 -0
- szl_spaces_surface.py +473 -0
Dockerfile
CHANGED
|
@@ -142,6 +142,10 @@ COPY knowledge.json ./static/knowledge.json
|
|
| 142 |
# Signed-off-by: Yachay <yachay@szlholdings.ai>
|
| 143 |
# ---------------------------------------------------------------------------
|
| 144 |
COPY knowledge.json szl_parity_gaps.py a11oy_warhacker_obs.py serve.py a11oy_wireA_metrics.py cathedral.html a11oy_operator_organ.py a11oy_hf_assets.py szl_b2_secdata.py gates_manifest.json a11oy_code_orchestrator.py a11oy_agent_loop.py a11oy_org_rag.py a11oy_mcp_client.py szl_rag.py a11oy_code_ide.html wayra_serve.py wayra_snapshot.json wayra_digests_7d.json szl_khipu_os_routes.py ./
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
COPY szl_khipu_consensus.py szl_puriq_formulas.py ayni_os_serve.py szl_live_wires.py live_wires.html live_wires_3d.js szl_dsse.py szl_provenance.py szl_be_hardening.py szl_unay.py szl_khipu_lmdb.py szl_khipu_replicate.py szl_unay_routes.py szl_warhacker_aliases.py a11oy_v4_hickok.py szl_khipu.py szl_formulas.py a11oy_v4_formulas.py szl_anatomy_3d.py szl_anatomy_routes.py ./
|
| 146 |
COPY _vendor_blobs.py szl_v4_fleet.py operator_shell_v4.py szl_bridge.py szl_bridge_schemas.py agent.html a11oy_bridge_cli.py szl_ken.py a11oy_formula_endpoints.py a11oy_formulas_page.py a11oy_frontier_patch.py a11oy_v4_agent.py szl_brain.py szl_wire.py szl_hub.py szl_rosie_companion.py szl_receipt_substrate.py szl_alloy_embed_fabric.py szl_ayni_quorum.py szl_agentic_loop.py ./
|
| 147 |
COPY szl_formula_wiring.py a11oy_code_engine.py a11oy_code.py a11oy_seismic.py szl_warhacker_real.py szl_warhacker_demos.py NOTICE_warhacker_demos.txt szl_llm_registry.py szl_elite_console.py szl_alloy_models.py szl_scaling.py szl_allodial.py szl_entanglement.py szl_neuroplasticity.py szl_chain_of_title.py szl_sovereign_compute.py a11oy_active_flux_router.py ./
|
|
|
|
| 142 |
# Signed-off-by: Yachay <yachay@szlholdings.ai>
|
| 143 |
# ---------------------------------------------------------------------------
|
| 144 |
COPY knowledge.json szl_parity_gaps.py a11oy_warhacker_obs.py serve.py a11oy_wireA_metrics.py cathedral.html a11oy_operator_organ.py a11oy_hf_assets.py szl_b2_secdata.py gates_manifest.json a11oy_code_orchestrator.py a11oy_agent_loop.py a11oy_org_rag.py a11oy_mcp_client.py szl_rag.py a11oy_code_ide.html wayra_serve.py wayra_snapshot.json wayra_digests_7d.json szl_khipu_os_routes.py ./
|
| 145 |
+
# Spaces-on-a11oy.net shared modules (Dev2+3) — reverse-proxy + console surface.
|
| 146 |
+
# Per-file COPY (this Dockerfile uses no `COPY . .`) or serve.py's guarded import
|
| 147 |
+
# falls back and /spaces + /api/<ns>/v1/spaces/health 404. Byte-identical a11oy+killinchu.
|
| 148 |
+
COPY szl_spaces_proxy.py szl_spaces_surface.py ./
|
| 149 |
COPY szl_khipu_consensus.py szl_puriq_formulas.py ayni_os_serve.py szl_live_wires.py live_wires.html live_wires_3d.js szl_dsse.py szl_provenance.py szl_be_hardening.py szl_unay.py szl_khipu_lmdb.py szl_khipu_replicate.py szl_unay_routes.py szl_warhacker_aliases.py a11oy_v4_hickok.py szl_khipu.py szl_formulas.py a11oy_v4_formulas.py szl_anatomy_3d.py szl_anatomy_routes.py ./
|
| 150 |
COPY _vendor_blobs.py szl_v4_fleet.py operator_shell_v4.py szl_bridge.py szl_bridge_schemas.py agent.html a11oy_bridge_cli.py szl_ken.py a11oy_formula_endpoints.py a11oy_formulas_page.py a11oy_frontier_patch.py a11oy_v4_agent.py szl_brain.py szl_wire.py szl_hub.py szl_rosie_companion.py szl_receipt_substrate.py szl_alloy_embed_fabric.py szl_ayni_quorum.py szl_agentic_loop.py ./
|
| 151 |
COPY szl_formula_wiring.py a11oy_code_engine.py a11oy_code.py a11oy_seismic.py szl_warhacker_real.py szl_warhacker_demos.py NOTICE_warhacker_demos.txt szl_llm_registry.py szl_elite_console.py szl_alloy_models.py szl_scaling.py szl_allodial.py szl_entanglement.py szl_neuroplasticity.py szl_chain_of_title.py szl_sovereign_compute.py a11oy_active_flux_router.py ./
|
szl_spaces_proxy.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
# © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173
|
| 4 |
+
"""szl_spaces_proxy.py — surface the live HF Spaces estate under a11oy.net/spaces/*.
|
| 5 |
+
|
| 6 |
+
ADDITIVE, self-contained, SHARED (byte-identical in a11oy + killinchu). Founder rule:
|
| 7 |
+
"all the huggingface on my a11oy.net" with NO new subdomains. This module reverse-
|
| 8 |
+
proxies each live Hugging Face Space to a same-origin path:
|
| 9 |
+
|
| 10 |
+
GET/HEAD /spaces -> index (delegates to szl_spaces_surface tiles if
|
| 11 |
+
present; else a minimal honest index of the 11)
|
| 12 |
+
GET/HEAD /spaces/{name} -> reverse-proxy of https://szlholdings-{name}.hf.space/
|
| 13 |
+
GET/HEAD /spaces/{name}/{path} -> reverse-proxy of that Space's sub-paths/assets
|
| 14 |
+
|
| 15 |
+
The proxy is a SERVER-SIDE fetch over the app's shared httpx.AsyncClient (resolved
|
| 16 |
+
lazily from serve.py's module global, like szl_engine_status), NOT a browser CDN load —
|
| 17 |
+
so it satisfies the 0-runtime-CDN doctrine for the same reason a11oy_hf_assets.py states
|
| 18 |
+
(server-side fetch, not a browser CDN). HTML responses get a <base href="/spaces/{name}/">
|
| 19 |
+
injected so the Space's root-relative assets resolve back through this proxy.
|
| 20 |
+
|
| 21 |
+
HONEST DEGRADE: upstream timeout / connect error / unreachable -> a clean 502 page
|
| 22 |
+
("Space <name> is starting / unreachable — open directly: <hf.space url>"). NEVER a fake
|
| 23 |
+
200. Hop-by-hop headers are stripped. The proxy is an ALLOWLIST of the known Space names
|
| 24 |
+
only (no open proxy); unknown names -> 404. a11oy is NOT self-proxied (would loop) and
|
| 25 |
+
killinchu is served on its own host — both are listed as TILES (by szl_spaces_surface)
|
| 26 |
+
linking to their canonical hosts, but are NOT in the reverse-proxy allowlist here.
|
| 27 |
+
|
| 28 |
+
No auth token is forwarded to HF — these are PUBLIC Spaces.
|
| 29 |
+
|
| 30 |
+
Routes are inserted at the FRONT of app.router.routes (app.router.routes[0:0] = new)
|
| 31 |
+
so they win over the /api/<ns>/{path:path} Node proxy + the /{full_path:path} SPA
|
| 32 |
+
catch-all (the same route-to-front idiom a11oy_hf_assets.py uses).
|
| 33 |
+
|
| 34 |
+
Doctrine v11: locked-proven = EXACTLY 8 {F1,F4,F7,F11,F12,F18,F19,F22} @ c7c0ba17;
|
| 35 |
+
Λ = Conjecture 1; Khipu = Conjecture 2; trust never 100%; 0 runtime CDN (server-side
|
| 36 |
+
fetch); no user-visible codenames (Space names are their own honest titles); never
|
| 37 |
+
commits a key; additive-only; never weakens a gate; honest 502 beats a fake 200.
|
| 38 |
+
|
| 39 |
+
Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>
|
| 40 |
+
Co-Authored-By: Perplexity Computer Agent <agent@perplexity.ai>
|
| 41 |
+
"""
|
| 42 |
+
from __future__ import annotations
|
| 43 |
+
|
| 44 |
+
import sys
|
| 45 |
+
from typing import Any
|
| 46 |
+
|
| 47 |
+
_ORG_PREFIX = "szlholdings-"
|
| 48 |
+
|
| 49 |
+
# The 11 live Spaces (all RUNNING). app URL pattern: https://szlholdings-<name>.hf.space
|
| 50 |
+
# PROXY allowlist = the names we reverse-proxy under /spaces/<name>. We SKIP:
|
| 51 |
+
# - "a11oy" : self-proxy would loop (a11oy.net IS the a11oy Space).
|
| 52 |
+
# - "killinchu": served on its own host; we link the tile to its host, never proxy it.
|
| 53 |
+
# Both skipped names are still listed as TILES by szl_spaces_surface.
|
| 54 |
+
ALL_SPACES = [
|
| 55 |
+
"immune", "sda", "anatomy", "cathedral", "energy", "yarqa",
|
| 56 |
+
"khipu-constellation", "llm-router-live", "hatun-mcp", "a11oy", "killinchu",
|
| 57 |
+
]
|
| 58 |
+
# Names we DO reverse-proxy (allowlist). Order preserved for the fallback index.
|
| 59 |
+
PROXY_SPACES = [n for n in ALL_SPACES if n not in ("a11oy", "killinchu")]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def hf_url(name: str) -> str:
|
| 63 |
+
"""Canonical HF app URL for a Space name (lowercase, org-prefixed)."""
|
| 64 |
+
return f"https://{_ORG_PREFIX}{name}.hf.space"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Hop-by-hop headers (RFC 7230 §6.1) — never forwarded across the proxy boundary.
|
| 68 |
+
_HOP_BY_HOP = {
|
| 69 |
+
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
| 70 |
+
"te", "trailers", "transfer-encoding", "upgrade",
|
| 71 |
+
# content-encoding/length are recomputed by our Response; strip to avoid mismatch.
|
| 72 |
+
"content-encoding", "content-length",
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
_PROXY_TIMEOUT = 8.0 # short timeout — honest 502 beats a hung request.
|
| 76 |
+
_PROXY_RETRIES = 1 # one quick retry on a transient flap before degrading.
|
| 77 |
+
_MAX_BYTES = 25 * 1024 * 1024 # 25MB cap per proxied response (Spaces are light pages).
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _honest_502(name: str) -> bytes:
|
| 81 |
+
"""Clean 502 page — NEVER a fake 200. Tells the user to open the Space directly."""
|
| 82 |
+
url = hf_url(name)
|
| 83 |
+
return (
|
| 84 |
+
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
|
| 85 |
+
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
| 86 |
+
"<title>Space %s — starting / unreachable</title></head>"
|
| 87 |
+
"<body style=\"margin:0;background:#0b0f14;color:#cdd6e0;"
|
| 88 |
+
"font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;"
|
| 89 |
+
"display:flex;min-height:100vh;align-items:center;justify-content:center\">"
|
| 90 |
+
"<main style=\"max-width:560px;padding:2rem;text-align:center\">"
|
| 91 |
+
"<div style=\"font-size:2.2rem;margin-bottom:.4rem\">⚡</div>"
|
| 92 |
+
"<h1 style=\"font-size:1.25rem;margin:0 0 .6rem;color:#e7eef6\">"
|
| 93 |
+
"Space “%s” is starting or unreachable</h1>"
|
| 94 |
+
"<p style=\"color:#8a96a3;margin:0 0 1.2rem\">The upstream Hugging Face Space did "
|
| 95 |
+
"not respond in time. This is an honest 502 — not a faked page. "
|
| 96 |
+
"It usually means the Space is waking from sleep; try again shortly.</p>"
|
| 97 |
+
"<p><a href=\"%s\" style=\"color:#d4a444;text-decoration:none;font-weight:600\">"
|
| 98 |
+
"Open %s directly on Hugging Face →</a></p>"
|
| 99 |
+
"<p style=\"margin-top:1.4rem\"><a href=\"/spaces\" "
|
| 100 |
+
"style=\"color:#7c8794;text-decoration:none\">← All Spaces</a></p>"
|
| 101 |
+
"</main></body></html>" % (name, name, url, name)
|
| 102 |
+
).encode("utf-8")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _fallback_index() -> bytes:
|
| 106 |
+
"""Minimal honest /spaces index — used ONLY if szl_spaces_surface (the rich tiles
|
| 107 |
+
page) is not registered. Lists every Space name with both links. 0 CDN."""
|
| 108 |
+
rows = []
|
| 109 |
+
for name in ALL_SPACES:
|
| 110 |
+
if name in ("a11oy", "killinchu"):
|
| 111 |
+
local = hf_url(name) # own canonical host (not self-proxied here)
|
| 112 |
+
local_label = "Open on HF"
|
| 113 |
+
else:
|
| 114 |
+
local = "/spaces/%s" % name
|
| 115 |
+
local_label = "Open in a11oy.net"
|
| 116 |
+
rows.append(
|
| 117 |
+
"<li style=\"margin:.4rem 0\"><strong style=\"color:#e7eef6\">%s</strong> "
|
| 118 |
+
"· <a href=\"%s\" style=\"color:#d4a444;text-decoration:none\">%s</a> "
|
| 119 |
+
"· <a href=\"%s\" style=\"color:#7c8794;text-decoration:none\">Open on HF</a>"
|
| 120 |
+
"</li>" % (name, local, local_label, hf_url(name))
|
| 121 |
+
)
|
| 122 |
+
return (
|
| 123 |
+
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
|
| 124 |
+
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
| 125 |
+
"<title>Spaces</title></head>"
|
| 126 |
+
"<body style=\"margin:0;background:#0b0f14;color:#cdd6e0;"
|
| 127 |
+
"font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:2rem\">"
|
| 128 |
+
"<main style=\"max-width:760px;margin:0 auto\">"
|
| 129 |
+
"<h1 style=\"color:#e7eef6\">Hugging Face Spaces</h1>"
|
| 130 |
+
"<p style=\"color:#8a96a3\">All live Spaces, surfaced under a11oy.net. "
|
| 131 |
+
"Server-side reverse proxy — 0 browser CDN.</p>"
|
| 132 |
+
"<ul style=\"list-style:none;padding:0\">" + "".join(rows) + "</ul>"
|
| 133 |
+
"</main></body></html>"
|
| 134 |
+
).encode("utf-8")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _resolve_client() -> Any:
|
| 138 |
+
"""Resolve the app's shared httpx.AsyncClient lazily from serve.py's module global
|
| 139 |
+
(same idiom as szl_engine_status) so registration order doesn't matter."""
|
| 140 |
+
try:
|
| 141 |
+
import serve as _serve # type: ignore
|
| 142 |
+
return getattr(_serve, "_http_client", None)
|
| 143 |
+
except Exception:
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _rewrite_html(body: bytes, name: str) -> bytes:
|
| 148 |
+
"""Inject <base href="/spaces/<name>/"> so the Space's root-relative asset URLs
|
| 149 |
+
(e.g. /assets/app.js, href="/"...) resolve back through this proxy. Idempotent:
|
| 150 |
+
if a base href for this proxy is already present, leave the body untouched."""
|
| 151 |
+
base_tag = ('<base href="/spaces/%s/">' % name).encode("utf-8")
|
| 152 |
+
if base_tag in body:
|
| 153 |
+
return body
|
| 154 |
+
low = body.lower()
|
| 155 |
+
# Prefer to place the <base> right after <head ...> so it applies to all assets.
|
| 156 |
+
h = low.find(b"<head")
|
| 157 |
+
if h != -1:
|
| 158 |
+
gt = body.find(b">", h)
|
| 159 |
+
if gt != -1:
|
| 160 |
+
return body[: gt + 1] + base_tag + body[gt + 1:]
|
| 161 |
+
# No <head> — try right after <html ...>.
|
| 162 |
+
h = low.find(b"<html")
|
| 163 |
+
if h != -1:
|
| 164 |
+
gt = body.find(b">", h)
|
| 165 |
+
if gt != -1:
|
| 166 |
+
return body[: gt + 1] + base_tag + body[gt + 1:]
|
| 167 |
+
# No usable anchor — prepend (still better than broken relative assets).
|
| 168 |
+
return base_tag + body
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
async def _proxy(name: str, subpath: str, request) -> Any:
|
| 172 |
+
"""Reverse-proxy one request to the named Space. Honest 502 on any upstream flap."""
|
| 173 |
+
from starlette.responses import Response
|
| 174 |
+
|
| 175 |
+
if name not in PROXY_SPACES:
|
| 176 |
+
# Not in the allowlist — either unknown, or a deliberately-skipped self/own-host
|
| 177 |
+
# Space. 404 (no open proxy). The skipped ones are reachable as tiles only.
|
| 178 |
+
return Response(content=b"Unknown or non-proxied Space.\n",
|
| 179 |
+
status_code=404, media_type="text/plain")
|
| 180 |
+
|
| 181 |
+
client = _resolve_client()
|
| 182 |
+
if client is None:
|
| 183 |
+
return Response(content=_honest_502(name), status_code=502,
|
| 184 |
+
media_type="text/html")
|
| 185 |
+
|
| 186 |
+
target = hf_url(name) + "/" + subpath
|
| 187 |
+
# Forward only safe request headers (drop Host so httpx sets the upstream host;
|
| 188 |
+
# drop hop-by-hop). No auth token forwarded — public Spaces.
|
| 189 |
+
fwd_headers = {
|
| 190 |
+
k: v for k, v in request.headers.items()
|
| 191 |
+
if k.lower() not in _HOP_BY_HOP and k.lower() not in ("host", "cookie")
|
| 192 |
+
}
|
| 193 |
+
method = request.method.upper()
|
| 194 |
+
|
| 195 |
+
upstream = None
|
| 196 |
+
last_exc = None
|
| 197 |
+
for _attempt in range(_PROXY_RETRIES + 1):
|
| 198 |
+
try:
|
| 199 |
+
upstream = await client.request(
|
| 200 |
+
method, target, headers=fwd_headers,
|
| 201 |
+
timeout=_PROXY_TIMEOUT, follow_redirects=True,
|
| 202 |
+
)
|
| 203 |
+
break
|
| 204 |
+
except Exception as e: # connect error / timeout / read error -> honest degrade
|
| 205 |
+
last_exc = e
|
| 206 |
+
upstream = None
|
| 207 |
+
|
| 208 |
+
if upstream is None:
|
| 209 |
+
print("[spaces-proxy] upstream flap for %s: %r" % (name, last_exc),
|
| 210 |
+
file=sys.stderr)
|
| 211 |
+
return Response(content=_honest_502(name), status_code=502,
|
| 212 |
+
media_type="text/html")
|
| 213 |
+
|
| 214 |
+
ct = (upstream.headers.get("content-type") or "").lower()
|
| 215 |
+
body = upstream.content or b""
|
| 216 |
+
if len(body) > _MAX_BYTES:
|
| 217 |
+
body = body[:_MAX_BYTES]
|
| 218 |
+
|
| 219 |
+
# HTML: inject <base href> so assets resolve under /spaces/<name>/.
|
| 220 |
+
if "text/html" in ct:
|
| 221 |
+
body = _rewrite_html(body, name)
|
| 222 |
+
|
| 223 |
+
out_headers = {
|
| 224 |
+
k: v for k, v in upstream.headers.items()
|
| 225 |
+
if k.lower() not in _HOP_BY_HOP
|
| 226 |
+
}
|
| 227 |
+
# HEAD must carry no body.
|
| 228 |
+
if method == "HEAD":
|
| 229 |
+
body = b""
|
| 230 |
+
|
| 231 |
+
return Response(content=body, status_code=upstream.status_code,
|
| 232 |
+
headers=out_headers,
|
| 233 |
+
media_type=upstream.headers.get("content-type"))
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def register(app, ns: str = "a11oy") -> str:
|
| 237 |
+
"""ADDITIVE: mount the /spaces reverse-proxy routes at the FRONT of the router so
|
| 238 |
+
they beat the SPA + Node-proxy catch-alls. try/except-guarded by the caller."""
|
| 239 |
+
try:
|
| 240 |
+
from starlette.responses import Response
|
| 241 |
+
except Exception as e: # pragma: no cover
|
| 242 |
+
return "unavailable: %r" % (e,)
|
| 243 |
+
|
| 244 |
+
n_before = len(app.router.routes)
|
| 245 |
+
|
| 246 |
+
async def _spaces_index(request):
|
| 247 |
+
# Delegate the rich tiles page to szl_spaces_surface if it owns /spaces; this
|
| 248 |
+
# fallback index only renders if that module isn't present. Idempotent: this
|
| 249 |
+
# route is inserted only once per register() and register() is guarded upstream.
|
| 250 |
+
if request.method.upper() == "HEAD":
|
| 251 |
+
return Response(content=b"", status_code=200, media_type="text/html")
|
| 252 |
+
return Response(content=_fallback_index(), status_code=200,
|
| 253 |
+
media_type="text/html")
|
| 254 |
+
|
| 255 |
+
async def _spaces_name(request):
|
| 256 |
+
name = request.path_params.get("name", "")
|
| 257 |
+
return await _proxy(name, "", request)
|
| 258 |
+
|
| 259 |
+
async def _spaces_path(request):
|
| 260 |
+
name = request.path_params.get("name", "")
|
| 261 |
+
subpath = request.path_params.get("path", "")
|
| 262 |
+
return await _proxy(name, subpath, request)
|
| 263 |
+
|
| 264 |
+
from starlette.routing import Route
|
| 265 |
+
# NOTE: szl_spaces_surface registers its OWN richer /spaces tiles page and inserts
|
| 266 |
+
# it at the front AFTER this module (it's registered second in serve.py), so the
|
| 267 |
+
# tiles page wins over this fallback index — by design. Both are idempotent.
|
| 268 |
+
routes = [
|
| 269 |
+
Route("/spaces", _spaces_index, methods=["GET", "HEAD"]),
|
| 270 |
+
Route("/spaces/{name}", _spaces_name, methods=["GET", "HEAD"]),
|
| 271 |
+
Route("/spaces/{name}/{path:path}", _spaces_path, methods=["GET", "HEAD"]),
|
| 272 |
+
]
|
| 273 |
+
for r in routes:
|
| 274 |
+
app.router.routes.append(r)
|
| 275 |
+
|
| 276 |
+
new = app.router.routes[n_before:]
|
| 277 |
+
del app.router.routes[n_before:]
|
| 278 |
+
app.router.routes[0:0] = new
|
| 279 |
+
print("[%s] Spaces reverse-proxy registered: /spaces + /spaces/{name} + "
|
| 280 |
+
"/spaces/{name}/{path} (%d proxied: %s) [moved %d routes to front]"
|
| 281 |
+
% (ns, len(PROXY_SPACES), ",".join(PROXY_SPACES), len(new)), file=sys.stderr)
|
| 282 |
+
return "ok: %d proxied spaces, %d routes" % (len(PROXY_SPACES), len(new))
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
# ---------------------------------------------------------------------------
|
| 286 |
+
# Self-test — pure stdlib + starlette TestClient; no real network. Stubs the shared
|
| 287 |
+
# client to assert: allowlist enforced (unknown -> 404), honest 502 on a dead client,
|
| 288 |
+
# <base href> injection, HEAD carries no body, and the fallback index lists all 11.
|
| 289 |
+
# ---------------------------------------------------------------------------
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
import ast as _ast
|
| 292 |
+
# parse-self guard
|
| 293 |
+
with open(__file__, "r", encoding="utf-8") as _fh:
|
| 294 |
+
_ast.parse(_fh.read())
|
| 295 |
+
|
| 296 |
+
# base-href injection
|
| 297 |
+
out = _rewrite_html(b"<html><head><title>x</title></head><body>hi</body></html>", "immune")
|
| 298 |
+
assert b'<base href="/spaces/immune/">' in out, "must inject base href"
|
| 299 |
+
assert _rewrite_html(out, "immune") == out, "base href injection must be idempotent"
|
| 300 |
+
|
| 301 |
+
# honest 502 content
|
| 302 |
+
p = _honest_502("sda")
|
| 303 |
+
assert b"honest 502" in p and b"szlholdings-sda.hf.space" in p, "502 must be honest + direct link"
|
| 304 |
+
|
| 305 |
+
# allowlist
|
| 306 |
+
assert "a11oy" not in PROXY_SPACES and "killinchu" not in PROXY_SPACES, "self/own-host skipped"
|
| 307 |
+
assert len(ALL_SPACES) == 11 and len(PROXY_SPACES) == 9, (len(ALL_SPACES), len(PROXY_SPACES))
|
| 308 |
+
|
| 309 |
+
# fallback index lists all 11 names
|
| 310 |
+
idx = _fallback_index()
|
| 311 |
+
for nm in ALL_SPACES:
|
| 312 |
+
assert nm.encode() in idx, "index missing %s" % nm
|
| 313 |
+
|
| 314 |
+
from starlette.applications import Starlette
|
| 315 |
+
from starlette.responses import PlainTextResponse
|
| 316 |
+
from starlette.routing import Route as _R
|
| 317 |
+
from starlette.testclient import TestClient
|
| 318 |
+
|
| 319 |
+
a = Starlette(routes=[_R("/{full_path:path}", lambda req: PlainTextResponse("SPA"))])
|
| 320 |
+
st = register(a, ns="a11oy")
|
| 321 |
+
assert st.startswith("ok:"), st
|
| 322 |
+
c = TestClient(a)
|
| 323 |
+
# /spaces fallback index resolves before the SPA catch-all and lists all names
|
| 324 |
+
r = c.get("/spaces")
|
| 325 |
+
assert r.status_code == 200 and "immune" in r.text and "killinchu" in r.text, r.status_code
|
| 326 |
+
# unknown / non-proxied name -> 404 (no open proxy); a11oy is deliberately skipped
|
| 327 |
+
assert c.get("/spaces/a11oy").status_code == 404, "a11oy must not be self-proxied"
|
| 328 |
+
assert c.get("/spaces/notreal").status_code == 404, "unknown -> 404"
|
| 329 |
+
# known name with no shared client wired -> honest 502 (never fake 200)
|
| 330 |
+
r502 = c.get("/spaces/immune")
|
| 331 |
+
assert r502.status_code == 502 and "honest 502" in r502.text, (r502.status_code, r502.text[:80])
|
| 332 |
+
# HEAD on a known name -> still 502 here (no client) but no body
|
| 333 |
+
rh = c.head("/spaces/immune")
|
| 334 |
+
assert rh.status_code == 502, rh.status_code
|
| 335 |
+
|
| 336 |
+
print("szl_spaces_proxy: ALL OK (allowlist; honest 502; base-href idempotent; "
|
| 337 |
+
"%d proxied of %d; 0 CDN server-side fetch)" % (len(PROXY_SPACES), len(ALL_SPACES)))
|
szl_spaces_surface.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
# © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173
|
| 4 |
+
"""szl_spaces_surface.py — console "Spaces" surface (health API + tiles + nav).
|
| 5 |
+
|
| 6 |
+
ADDITIVE, self-contained, SHARED (byte-identical in a11oy + killinchu). The console
|
| 7 |
+
companion to szl_spaces_proxy: a LIVE health view of the whole HF Spaces estate plus a
|
| 8 |
+
clean tiles page and ONE idempotent nav item, following the additive-injector pattern
|
| 9 |
+
(a11oy_nav_wireup.py / killinchu_nav_wireup.py).
|
| 10 |
+
|
| 11 |
+
ROUTES (additive, inserted at the FRONT of the router so they beat the SPA + Node-proxy
|
| 12 |
+
catch-alls — same route-to-front idiom as a11oy_hf_assets.py):
|
| 13 |
+
|
| 14 |
+
GET /api/<ns>/v1/spaces/health -> for each Space, an HONEST status:
|
| 15 |
+
{name, title, stage, app_reachable, url, proxy_url}
|
| 16 |
+
- stage : runtime.stage from the HF API
|
| 17 |
+
(https://huggingface.co/api/spaces/SZLHOLDINGS/<name>),
|
| 18 |
+
or "unknown" if that API call degrades. LABELLED as HF-API.
|
| 19 |
+
- app_reachable : a REAL same-origin server-side probe of
|
| 20 |
+
https://szlholdings-<name>.hf.space/ (HEAD, short timeout).
|
| 21 |
+
true ONLY when the probe really succeeded. Never fabricated.
|
| 22 |
+
Degrade -> stage:"unknown", app_reachable:false. NEVER a faked stage/200.
|
| 23 |
+
|
| 24 |
+
GET/HEAD /spaces -> a clean tiles page (one card per Space: honest
|
| 25 |
+
title, live status dot fed by /health, "Open in a11oy.net" -> /spaces/<name>,
|
| 26 |
+
"Open on HF" -> hf.space url). Pure inline markup, 0 browser CDN. The status
|
| 27 |
+
dots are filled by a tiny inline fetch of the SAME-ORIGIN /health JSON (not a
|
| 28 |
+
CDN; the data is our own server-side-probed endpoint).
|
| 29 |
+
|
| 30 |
+
NAV: a BaseHTTPMiddleware injector adds ONE nav item "Spaces" -> /spaces into the
|
| 31 |
+
console left-nav (before <div class="side-foot">, with nav-group / nav-item fallbacks).
|
| 32 |
+
Idempotent — keyed by data-attribute data-nav-spaces="hf1"; removes NOTHING from the SPA
|
| 33 |
+
source; never touches /api, /v1, /assets, /static, /vendor responses. Works identically
|
| 34 |
+
on the a11oy /console and killinchu /elite consoles (same sidebar markup) — which is why
|
| 35 |
+
this module can be byte-identical in both apps.
|
| 36 |
+
|
| 37 |
+
The /spaces probe + HF-API call are SERVER-SIDE fetches (resolve the shared httpx client
|
| 38 |
+
lazily from serve.py, like szl_engine_status) — 0 runtime CDN, same justification as
|
| 39 |
+
a11oy_hf_assets.py. No auth token forwarded to HF (public Spaces / public API).
|
| 40 |
+
|
| 41 |
+
Doctrine v11: locked-proven = EXACTLY 8 {F1,F4,F7,F11,F12,F18,F19,F22} @ c7c0ba17;
|
| 42 |
+
Λ = Conjecture 1; Khipu = Conjecture 2; trust never 100%; 0 runtime CDN; no user-visible
|
| 43 |
+
codenames (Space names are their own honest titles); never commits a key; additive-only;
|
| 44 |
+
honest "unknown"/false beats a fabricated stage.
|
| 45 |
+
|
| 46 |
+
Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>
|
| 47 |
+
Co-Authored-By: Perplexity Computer Agent <agent@perplexity.ai>
|
| 48 |
+
"""
|
| 49 |
+
from __future__ import annotations
|
| 50 |
+
|
| 51 |
+
import sys
|
| 52 |
+
import time
|
| 53 |
+
from typing import Any
|
| 54 |
+
|
| 55 |
+
_ORG = "SZLHOLDINGS"
|
| 56 |
+
_ORG_PREFIX = "szlholdings-"
|
| 57 |
+
|
| 58 |
+
# The 11 live Spaces + their honest, public titles (NOT codenames — each Space's own
|
| 59 |
+
# name). a11oy + killinchu are listed as tiles linking to their own canonical hosts;
|
| 60 |
+
# they are NOT reverse-proxied by szl_spaces_proxy (self / own-host).
|
| 61 |
+
SPACES: list[dict[str, str]] = [
|
| 62 |
+
{"name": "immune", "title": "Immune — Verifiable Screening"},
|
| 63 |
+
{"name": "sda", "title": "SDA — Space Domain Awareness"},
|
| 64 |
+
{"name": "anatomy", "title": "Anatomy — Canonical Formula Registry"},
|
| 65 |
+
{"name": "cathedral", "title": "Cathedral — Estate Cathedral"},
|
| 66 |
+
{"name": "energy", "title": "Energy — Sovereign Compute"},
|
| 67 |
+
{"name": "yarqa", "title": "Yarqa — Data Channel"},
|
| 68 |
+
{"name": "khipu-constellation", "title": "Khipu Constellation — Receipt Mesh"},
|
| 69 |
+
{"name": "llm-router-live", "title": "LLM Router (Live)"},
|
| 70 |
+
{"name": "hatun-mcp", "title": "Hatun — MCP Server"},
|
| 71 |
+
{"name": "a11oy", "title": "a11oy — Brand Orchestration Layer"},
|
| 72 |
+
{"name": "killinchu", "title": "Killinchu — Edge Console"},
|
| 73 |
+
]
|
| 74 |
+
# Spaces served on their OWN canonical host (not reverse-proxied here): the tile's
|
| 75 |
+
# primary "open" link points at HF rather than /spaces/<name>.
|
| 76 |
+
_OWN_HOST = {"a11oy", "killinchu"}
|
| 77 |
+
|
| 78 |
+
_DOCTRINE = {
|
| 79 |
+
"version": "v11",
|
| 80 |
+
"lambda": "Conjecture 1",
|
| 81 |
+
"khipu": "Conjecture 2",
|
| 82 |
+
"locked_proven": ["F1", "F4", "F7", "F11", "F12", "F18", "F19", "F22"],
|
| 83 |
+
"trust_ceiling": "never 100%",
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
_PROBE_TIMEOUT = 6.0
|
| 87 |
+
_HF_API_TIMEOUT = 6.0
|
| 88 |
+
_HEALTH_CACHE_TTL = 20.0 # seconds — keep the tiles page snappy without re-probing 11x.
|
| 89 |
+
_HEALTH_CACHE: dict[str, Any] = {"ts": 0.0, "payload": None}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def hf_url(name: str) -> str:
|
| 93 |
+
return f"https://{_ORG_PREFIX}{name}.hf.space"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def hf_api_url(name: str) -> str:
|
| 97 |
+
return f"https://huggingface.co/api/spaces/{_ORG}/{name}"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def proxy_url(name: str) -> str:
|
| 101 |
+
"""Where the tile's primary "open" link points. Own-host Spaces -> their HF host;
|
| 102 |
+
everyone else -> the same-origin reverse proxy at /spaces/<name>."""
|
| 103 |
+
return hf_url(name) if name in _OWN_HOST else f"/spaces/{name}"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _resolve_client() -> Any:
|
| 107 |
+
"""Lazily resolve the app's shared httpx.AsyncClient from serve.py (same idiom as
|
| 108 |
+
szl_engine_status), so registration order doesn't matter."""
|
| 109 |
+
try:
|
| 110 |
+
import serve as _serve # type: ignore
|
| 111 |
+
return getattr(_serve, "_http_client", None)
|
| 112 |
+
except Exception:
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
async def _probe_one(client: Any, sp: dict[str, str]) -> dict[str, Any]:
|
| 117 |
+
"""HONEST per-Space status. app_reachable is a REAL HEAD probe; stage is from the
|
| 118 |
+
HF API. Any failure degrades to honest false/'unknown' — never fabricated."""
|
| 119 |
+
name = sp["name"]
|
| 120 |
+
result: dict[str, Any] = {
|
| 121 |
+
"name": name,
|
| 122 |
+
"title": sp["title"],
|
| 123 |
+
"url": hf_url(name),
|
| 124 |
+
"proxy_url": proxy_url(name),
|
| 125 |
+
"own_host": name in _OWN_HOST,
|
| 126 |
+
"stage": "unknown", # from HF API; HF-API-labelled below
|
| 127 |
+
"stage_source": "hf-api",
|
| 128 |
+
"app_reachable": False, # REAL probe; only true when the probe truly succeeds
|
| 129 |
+
}
|
| 130 |
+
if client is None:
|
| 131 |
+
result["note"] = "no http client; honest degrade"
|
| 132 |
+
return result
|
| 133 |
+
|
| 134 |
+
# (1) REAL same-origin liveness probe of the Space app (HEAD; fall back to tiny GET).
|
| 135 |
+
try:
|
| 136 |
+
r = await client.request("HEAD", hf_url(name) + "/",
|
| 137 |
+
timeout=_PROBE_TIMEOUT, follow_redirects=True)
|
| 138 |
+
result["app_reachable"] = bool(r.status_code < 500)
|
| 139 |
+
result["app_status"] = r.status_code
|
| 140 |
+
except Exception:
|
| 141 |
+
try:
|
| 142 |
+
r = await client.get(hf_url(name) + "/", timeout=_PROBE_TIMEOUT,
|
| 143 |
+
follow_redirects=True)
|
| 144 |
+
result["app_reachable"] = bool(r.status_code < 500)
|
| 145 |
+
result["app_status"] = r.status_code
|
| 146 |
+
except Exception as e:
|
| 147 |
+
result["app_reachable"] = False
|
| 148 |
+
result["probe_error"] = type(e).__name__
|
| 149 |
+
|
| 150 |
+
# (2) HF API runtime.stage (public API, no token). Honest "unknown" on any failure.
|
| 151 |
+
try:
|
| 152 |
+
ra = await client.get(hf_api_url(name), timeout=_HF_API_TIMEOUT,
|
| 153 |
+
headers={"User-Agent": "szl-spaces-surface/1.0"})
|
| 154 |
+
if ra.status_code == 200:
|
| 155 |
+
data = ra.json()
|
| 156 |
+
stage = (((data or {}).get("runtime") or {}).get("stage"))
|
| 157 |
+
if isinstance(stage, str) and stage:
|
| 158 |
+
result["stage"] = stage
|
| 159 |
+
else:
|
| 160 |
+
result["stage_http"] = ra.status_code
|
| 161 |
+
except Exception as e:
|
| 162 |
+
result["stage_error"] = type(e).__name__
|
| 163 |
+
|
| 164 |
+
return result
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
async def spaces_health() -> dict[str, Any]:
|
| 168 |
+
"""Aggregate honest health for all 11 Spaces (short TTL cache)."""
|
| 169 |
+
now = time.monotonic()
|
| 170 |
+
if _HEALTH_CACHE["payload"] is not None and (now - _HEALTH_CACHE["ts"]) < _HEALTH_CACHE_TTL:
|
| 171 |
+
return _HEALTH_CACHE["payload"]
|
| 172 |
+
|
| 173 |
+
client = _resolve_client()
|
| 174 |
+
spaces: list[dict[str, Any]] = []
|
| 175 |
+
if client is None:
|
| 176 |
+
for sp in SPACES:
|
| 177 |
+
spaces.append({
|
| 178 |
+
"name": sp["name"], "title": sp["title"], "url": hf_url(sp["name"]),
|
| 179 |
+
"proxy_url": proxy_url(sp["name"]), "own_host": sp["name"] in _OWN_HOST,
|
| 180 |
+
"stage": "unknown", "stage_source": "hf-api", "app_reachable": False,
|
| 181 |
+
"note": "no http client; honest degrade",
|
| 182 |
+
})
|
| 183 |
+
else:
|
| 184 |
+
import asyncio as _asyncio
|
| 185 |
+
spaces = await _asyncio.gather(*[_probe_one(client, sp) for sp in SPACES])
|
| 186 |
+
spaces = list(spaces)
|
| 187 |
+
|
| 188 |
+
payload = {
|
| 189 |
+
"count": len(spaces),
|
| 190 |
+
"spaces": spaces,
|
| 191 |
+
"labels": {
|
| 192 |
+
"stage": "HF API runtime.stage (https://huggingface.co/api/spaces/SZLHOLDINGS/<name>)",
|
| 193 |
+
"app_reachable": "REAL same-origin server-side HEAD/GET probe of the Space app",
|
| 194 |
+
"degrade": "stage:'unknown' + app_reachable:false; never fabricated",
|
| 195 |
+
},
|
| 196 |
+
"note": "Server-side probed; 0 browser CDN. Honest live/unknown only.",
|
| 197 |
+
"doctrine": _DOCTRINE,
|
| 198 |
+
"fetchedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
| 199 |
+
}
|
| 200 |
+
_HEALTH_CACHE["payload"] = payload
|
| 201 |
+
_HEALTH_CACHE["ts"] = now
|
| 202 |
+
return payload
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# ---------------------------------------------------------------------------
|
| 206 |
+
# Tiles page — pure inline markup, 0 CDN. Status dots are filled by a tiny inline
|
| 207 |
+
# fetch of the SAME-ORIGIN /api/<ns>/v1/spaces/health (our own server-side-probed
|
| 208 |
+
# endpoint, not a CDN). Cards are pre-rendered so the page is useful even with JS off.
|
| 209 |
+
# ---------------------------------------------------------------------------
|
| 210 |
+
def _tiles_page(ns: str) -> bytes:
|
| 211 |
+
cards = []
|
| 212 |
+
for sp in SPACES:
|
| 213 |
+
name = sp["name"]
|
| 214 |
+
title = sp["title"]
|
| 215 |
+
primary = proxy_url(name)
|
| 216 |
+
primary_label = "Open on HF" if name in _OWN_HOST else "Open in a11oy.net"
|
| 217 |
+
cards.append(
|
| 218 |
+
'<article class="sp-card" data-space="%s">'
|
| 219 |
+
'<header class="sp-head">'
|
| 220 |
+
'<span class="sp-dot" data-dot="%s" title="status">●</span>'
|
| 221 |
+
'<h2 class="sp-title">%s</h2></header>'
|
| 222 |
+
'<div class="sp-stage" data-stage="%s">stage: <span>checking…</span></div>'
|
| 223 |
+
'<div class="sp-links">'
|
| 224 |
+
'<a class="sp-open" href="%s">%s</a>'
|
| 225 |
+
'<a class="sp-hf" href="%s" rel="noopener" target="_blank">Open on HF ↗</a>'
|
| 226 |
+
'</div></article>'
|
| 227 |
+
% (name, name, title, name, primary, primary_label, hf_url(name))
|
| 228 |
+
)
|
| 229 |
+
html = (
|
| 230 |
+
'<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
| 231 |
+
'<meta name="viewport" content="width=device-width,initial-scale=1">'
|
| 232 |
+
'<title>Spaces · a11oy</title>'
|
| 233 |
+
'<style>'
|
| 234 |
+
':root{color-scheme:dark}'
|
| 235 |
+
'*{box-sizing:border-box}'
|
| 236 |
+
'body{margin:0;background:#0b0f14;color:#cdd6e0;'
|
| 237 |
+
'font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}'
|
| 238 |
+
'.sp-wrap{max-width:1100px;margin:0 auto;padding:2rem 1.25rem}'
|
| 239 |
+
'.sp-h1{color:#e7eef6;font-size:1.6rem;margin:0 0 .25rem}'
|
| 240 |
+
'.sp-sub{color:#8a96a3;margin:0 0 1.6rem}'
|
| 241 |
+
'.sp-grid{display:grid;gap:1rem;'
|
| 242 |
+
'grid-template-columns:repeat(auto-fill,minmax(260px,1fr))}'
|
| 243 |
+
'.sp-card{background:#121821;border:1px solid #1d2632;border-radius:12px;'
|
| 244 |
+
'padding:1rem 1.1rem;display:flex;flex-direction:column;gap:.5rem}'
|
| 245 |
+
'.sp-head{display:flex;align-items:center;gap:.55rem}'
|
| 246 |
+
'.sp-dot{color:#5b6675;font-size:.7rem;line-height:1}'
|
| 247 |
+
'.sp-dot.up{color:#3ad07a}.sp-dot.down{color:#e0593a}.sp-dot.unknown{color:#c9a23a}'
|
| 248 |
+
'.sp-title{font-size:1rem;margin:0;color:#e7eef6;font-weight:600}'
|
| 249 |
+
'.sp-stage{color:#7c8794;font-size:.82rem}'
|
| 250 |
+
'.sp-stage span{color:#9fb0c0}'
|
| 251 |
+
'.sp-links{margin-top:auto;display:flex;gap:.9rem;flex-wrap:wrap;padding-top:.4rem}'
|
| 252 |
+
'.sp-open{color:#d4a444;text-decoration:none;font-weight:600}'
|
| 253 |
+
'.sp-hf{color:#7c8794;text-decoration:none}'
|
| 254 |
+
'.sp-foot{color:#5b6675;font-size:.8rem;margin-top:1.6rem}'
|
| 255 |
+
'</style></head>'
|
| 256 |
+
'<body><main class="sp-wrap">'
|
| 257 |
+
'<h1 class="sp-h1">Hugging Face Spaces</h1>'
|
| 258 |
+
'<p class="sp-sub">All live Spaces, surfaced on a11oy.net — server-side '
|
| 259 |
+
'reverse proxy + honest live health. 0 browser CDN.</p>'
|
| 260 |
+
'<div class="sp-grid">' + "".join(cards) + '</div>'
|
| 261 |
+
'<p class="sp-foot">Status dot & stage are filled from the same-origin '
|
| 262 |
+
'<code>/api/' + ns + '/v1/spaces/health</code> endpoint (real server-side probe '
|
| 263 |
+
'+ HF API). Honest: a grey/amber dot means starting or unknown, never a faked up.</p>'
|
| 264 |
+
'</main>'
|
| 265 |
+
'<script>'
|
| 266 |
+
'(function(){'
|
| 267 |
+
'fetch("/api/' + ns + '/v1/spaces/health").then(function(r){return r.json();})'
|
| 268 |
+
'.then(function(d){(d.spaces||[]).forEach(function(s){'
|
| 269 |
+
'var card=document.querySelector(\'[data-space="\'+s.name+\'"]\');if(!card)return;'
|
| 270 |
+
'var dot=card.querySelector(".sp-dot");'
|
| 271 |
+
'if(dot){dot.classList.remove("up","down","unknown");'
|
| 272 |
+
'dot.classList.add(s.app_reachable?"up":(s.stage&&s.stage!=="unknown"?"unknown":"down"));'
|
| 273 |
+
'dot.title=(s.app_reachable?"reachable":"unreachable")+" / stage:"+(s.stage||"unknown");}'
|
| 274 |
+
'var st=card.querySelector(".sp-stage span");'
|
| 275 |
+
'if(st){st.textContent=(s.stage||"unknown")+(s.app_reachable?" \\u00b7 reachable":" \\u00b7 unreachable");}'
|
| 276 |
+
'});}).catch(function(){});'
|
| 277 |
+
'})();'
|
| 278 |
+
'</script>'
|
| 279 |
+
'</body></html>'
|
| 280 |
+
)
|
| 281 |
+
return html.encode("utf-8")
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# ---------------------------------------------------------------------------
|
| 285 |
+
# Nav injector — ONE "Spaces" nav item into the console left-nav. Same additive,
|
| 286 |
+
# idempotent BaseHTTPMiddleware idiom as a11oy_nav_wireup / killinchu_nav_wireup.
|
| 287 |
+
# ---------------------------------------------------------------------------
|
| 288 |
+
_NAV_MARKER = b'data-nav-spaces="hf1"'
|
| 289 |
+
_FOOT_ANCHOR = b'<div class="side-foot">'
|
| 290 |
+
_GROUP_ANCHOR = b'<div class="nav-group">'
|
| 291 |
+
_NAVITEM_ANCHOR = b'<div class="nav-item"'
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def _nav_item() -> bytes:
|
| 295 |
+
"""ONE 'Spaces' nav item, mirroring the console's own nav-item markup so it inherits
|
| 296 |
+
the console styling (0 CDN, 0 codename). Globe glyph; honest label."""
|
| 297 |
+
return (
|
| 298 |
+
'<div class="nav-item" data-nav-spaces="hf1" data-wireup-path="/spaces" '
|
| 299 |
+
'onclick="location.href=\'/spaces\'" style="cursor:pointer">'
|
| 300 |
+
'<span class="ico">\U0001F310</span>Spaces</div>'
|
| 301 |
+
).encode("utf-8")
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _make_injector():
|
| 305 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 306 |
+
from starlette.responses import Response
|
| 307 |
+
|
| 308 |
+
nav_item = _nav_item()
|
| 309 |
+
|
| 310 |
+
class _SpacesNavInjector(BaseHTTPMiddleware):
|
| 311 |
+
async def dispatch(self, request, call_next):
|
| 312 |
+
resp = await call_next(request)
|
| 313 |
+
try:
|
| 314 |
+
ct = (resp.headers.get("content-type") or "").lower()
|
| 315 |
+
if "text/html" not in ct:
|
| 316 |
+
return resp
|
| 317 |
+
p = request.url.path
|
| 318 |
+
if (p.startswith("/api/") or p.startswith("/v1/")
|
| 319 |
+
or p.startswith("/vendor/") or p.startswith("/assets/")
|
| 320 |
+
or p.startswith("/static/")):
|
| 321 |
+
return resp
|
| 322 |
+
# The /spaces tiles page IS our own page; don't inject the sidebar nav
|
| 323 |
+
# into it (it has no console sidebar). Cheap guard — also keeps it idempotent.
|
| 324 |
+
if p == "/spaces" or p.startswith("/spaces/"):
|
| 325 |
+
return resp
|
| 326 |
+
|
| 327 |
+
body = b""
|
| 328 |
+
async for chunk in resp.body_iterator:
|
| 329 |
+
body += chunk if isinstance(chunk, (bytes, bytearray)) else str(chunk).encode()
|
| 330 |
+
|
| 331 |
+
if _NAV_MARKER not in body:
|
| 332 |
+
if _FOOT_ANCHOR in body:
|
| 333 |
+
body = body.replace(_FOOT_ANCHOR, nav_item + _FOOT_ANCHOR, 1)
|
| 334 |
+
elif _NAVITEM_ANCHOR in body:
|
| 335 |
+
# No footer found, but a nav exists -> place after the first
|
| 336 |
+
# nav-item div so 'Spaces' still lands in the nav.
|
| 337 |
+
start = body.find(_NAVITEM_ANCHOR)
|
| 338 |
+
end = body.find(b"</div>", start)
|
| 339 |
+
if end != -1:
|
| 340 |
+
end += len(b"</div>")
|
| 341 |
+
body = body[:end] + nav_item + body[end:]
|
| 342 |
+
elif _GROUP_ANCHOR in body:
|
| 343 |
+
body = body.replace(_GROUP_ANCHOR, _GROUP_ANCHOR + nav_item, 1)
|
| 344 |
+
|
| 345 |
+
# body_iterator is consumed — MUST rebuild the Response even if unchanged,
|
| 346 |
+
# else downstream sees an empty body (white screen).
|
| 347 |
+
headers = dict(resp.headers)
|
| 348 |
+
headers.pop("content-length", None)
|
| 349 |
+
return Response(content=body, status_code=resp.status_code,
|
| 350 |
+
headers=headers, media_type="text/html")
|
| 351 |
+
except Exception:
|
| 352 |
+
return resp
|
| 353 |
+
|
| 354 |
+
return _SpacesNavInjector
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def register(app, ns: str = "a11oy") -> str:
|
| 358 |
+
"""ADDITIVE: mount GET /api/<ns>/v1/spaces/health + GET/HEAD /spaces (rich tiles)
|
| 359 |
+
at the FRONT of the router (beat the SPA + Node-proxy catch-alls), and attach the
|
| 360 |
+
idempotent 'Spaces' nav injector. try/except-guarded by the caller."""
|
| 361 |
+
try:
|
| 362 |
+
from fastapi.responses import JSONResponse # noqa: F401
|
| 363 |
+
from starlette.responses import Response, JSONResponse as _JSON
|
| 364 |
+
except Exception as e: # pragma: no cover
|
| 365 |
+
return "unavailable: %r" % (e,)
|
| 366 |
+
|
| 367 |
+
n_before = len(app.router.routes)
|
| 368 |
+
tiles = _tiles_page(ns)
|
| 369 |
+
|
| 370 |
+
async def _health(request):
|
| 371 |
+
payload = await spaces_health()
|
| 372 |
+
return _JSON(payload)
|
| 373 |
+
|
| 374 |
+
async def _tiles(request):
|
| 375 |
+
if request.method.upper() == "HEAD":
|
| 376 |
+
return Response(content=b"", status_code=200, media_type="text/html")
|
| 377 |
+
return Response(content=tiles, status_code=200, media_type="text/html")
|
| 378 |
+
|
| 379 |
+
from starlette.routing import Route
|
| 380 |
+
routes = [
|
| 381 |
+
Route("/api/%s/v1/spaces/health" % ns, _health, methods=["GET"]),
|
| 382 |
+
# The rich tiles page OWNS /spaces (wins over szl_spaces_proxy's fallback index
|
| 383 |
+
# because this module is registered SECOND and front-inserts after it).
|
| 384 |
+
Route("/spaces", _tiles, methods=["GET", "HEAD"]),
|
| 385 |
+
]
|
| 386 |
+
for r in routes:
|
| 387 |
+
app.router.routes.append(r)
|
| 388 |
+
|
| 389 |
+
new = app.router.routes[n_before:]
|
| 390 |
+
del app.router.routes[n_before:]
|
| 391 |
+
app.router.routes[0:0] = new
|
| 392 |
+
|
| 393 |
+
app.add_middleware(_make_injector())
|
| 394 |
+
|
| 395 |
+
print("[%s] Spaces surface registered: /api/%s/v1/spaces/health + /spaces (tiles, "
|
| 396 |
+
"%d spaces) + nav injector [moved %d routes to front]"
|
| 397 |
+
% (ns, ns, len(SPACES), len(new)), file=sys.stderr)
|
| 398 |
+
return "ok: %d spaces, health + tiles + nav, %d routes" % (len(SPACES), len(new))
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# ---------------------------------------------------------------------------
|
| 402 |
+
# Self-test — pure stdlib + starlette TestClient; no real network. Stubs the shared
|
| 403 |
+
# client to assert: health degrades honestly (no client -> stage unknown + reachable
|
| 404 |
+
# false), tiles page lists ALL 11 names + has the health-fetch JS, nav injects exactly
|
| 405 |
+
# once + is idempotent + removes nothing, /spaces is NOT nav-injected.
|
| 406 |
+
# ---------------------------------------------------------------------------
|
| 407 |
+
if __name__ == "__main__":
|
| 408 |
+
import ast as _ast
|
| 409 |
+
with open(__file__, "r", encoding="utf-8") as _fh:
|
| 410 |
+
_ast.parse(_fh.read())
|
| 411 |
+
|
| 412 |
+
assert len(SPACES) == 11, len(SPACES)
|
| 413 |
+
tp = _tiles_page("a11oy")
|
| 414 |
+
for sp in SPACES:
|
| 415 |
+
assert sp["name"].encode() in tp, "tiles missing %s" % sp["name"]
|
| 416 |
+
assert sp["title"].encode() in tp, "tiles missing title %s" % sp["title"]
|
| 417 |
+
assert b"/api/a11oy/v1/spaces/health" in tp, "tiles must fetch the health endpoint"
|
| 418 |
+
assert b"http://" not in tp, "tiles must be 0 CDN (no http://)"
|
| 419 |
+
# the only https in the page should be the hf.space open links (server-named, fine)
|
| 420 |
+
assert tp.count(b"https://huggingface.co") == 0, "tiles must not pull from HF CDN"
|
| 421 |
+
|
| 422 |
+
from starlette.applications import Starlette
|
| 423 |
+
from starlette.responses import HTMLResponse, PlainTextResponse
|
| 424 |
+
from starlette.routing import Route as _R
|
| 425 |
+
from starlette.testclient import TestClient
|
| 426 |
+
|
| 427 |
+
SAMPLE_CONSOLE = (
|
| 428 |
+
'<html><body><aside>'
|
| 429 |
+
'<div class="nav-group">Operate</div>'
|
| 430 |
+
'<div class="nav-item" data-view="x" onclick="go(\'x\')">'
|
| 431 |
+
'<span class="ico">+</span>Existing</div>'
|
| 432 |
+
'<div class="side-foot">footer</div>'
|
| 433 |
+
'</aside></body></html>'
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
async def _console(req):
|
| 437 |
+
return HTMLResponse(SAMPLE_CONSOLE)
|
| 438 |
+
|
| 439 |
+
app = Starlette(routes=[
|
| 440 |
+
_R("/console", _console),
|
| 441 |
+
_R("/{full_path:path}", lambda req: PlainTextResponse("SPA")),
|
| 442 |
+
])
|
| 443 |
+
st = register(app, ns="a11oy")
|
| 444 |
+
assert st.startswith("ok:"), st
|
| 445 |
+
c = TestClient(app)
|
| 446 |
+
|
| 447 |
+
# health: no client wired -> honest degrade for all 11
|
| 448 |
+
h = c.get("/api/a11oy/v1/spaces/health").json()
|
| 449 |
+
assert h["count"] == 11, h["count"]
|
| 450 |
+
assert all(s["stage"] == "unknown" and s["app_reachable"] is False for s in h["spaces"]), \
|
| 451 |
+
"no client must degrade to unknown/false (never fabricated)"
|
| 452 |
+
assert h["doctrine"]["locked_proven"] == ["F1", "F4", "F7", "F11", "F12", "F18", "F19", "F22"]
|
| 453 |
+
|
| 454 |
+
# tiles page resolves + lists all names
|
| 455 |
+
t = c.get("/spaces")
|
| 456 |
+
assert t.status_code == 200 and "text/html" in t.headers["content-type"], t.status_code
|
| 457 |
+
for sp in SPACES:
|
| 458 |
+
assert sp["name"] in t.text, "tiles page missing %s" % sp["name"]
|
| 459 |
+
|
| 460 |
+
# nav injects exactly once + idempotent + removes nothing
|
| 461 |
+
p1 = c.get("/console").text
|
| 462 |
+
p2 = c.get("/console").text
|
| 463 |
+
assert p1.count('data-nav-spaces="hf1"') == 1, "nav must inject exactly once"
|
| 464 |
+
assert p2.count('data-nav-spaces="hf1"') == 1, "nav must be idempotent"
|
| 465 |
+
assert "location.href='/spaces'" in p1, "nav must link /spaces"
|
| 466 |
+
assert "Operate</div>" in p1 and "Existing</div>" in p1 and "footer</div>" in p1, \
|
| 467 |
+
"must remove nothing from the SPA source"
|
| 468 |
+
assert p1 == p2, "second console render must be byte-identical (idempotent)"
|
| 469 |
+
# the tiles page itself must NOT be nav-injected (it has no console sidebar)
|
| 470 |
+
assert 'data-nav-spaces="hf1"' not in c.get("/spaces").text, "/spaces must not be nav-injected"
|
| 471 |
+
|
| 472 |
+
print("szl_spaces_surface: ALL OK (11 spaces; honest degrade; tiles 0-CDN; nav "
|
| 473 |
+
"idempotent + additive; /spaces not self-injected)")
|