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): routers/__init__.py, routers/frontier_reads.py, routers/series_a_control_plane.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.
- routers/__init__.py +11 -4
- routers/frontier_reads.py +28 -9
- routers/series_a_control_plane.py +991 -0
routers/__init__.py
CHANGED
|
@@ -18,9 +18,11 @@ SAME lexical position the routes used to occupy — i.e. BEFORE the SPA
|
|
| 18 |
* a missing/broken group can NEVER take down the SPA (guarded), and
|
| 19 |
* the register() functions are IMPORTED + CALLED (register-invocation-guard clean).
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
The package top-level name is `routers` (not szl_*/a11oy_*), so it is intentionally
|
| 26 |
OUTSIDE the guarded-import-liveness first-party scan — and the files exist anyway.
|
|
@@ -28,4 +30,9 @@ OUTSIDE the guarded-import-liveness first-party scan — and the files exist any
|
|
| 28 |
Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
|
| 29 |
"""
|
| 30 |
|
| 31 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
* a missing/broken group can NEVER take down the SPA (guarded), and
|
| 19 |
* the register() functions are IMPORTED + CALLED (register-invocation-guard clean).
|
| 20 |
|
| 21 |
+
The additive `series_a_control_plane` module is not a refactor-only route group. It
|
| 22 |
+
is the single governed integration seam for current estate truth, Counterfactual
|
| 23 |
+
Action Passports, signed receipts, and bounded one-attempt effectors. It is exported
|
| 24 |
+
here so invocation and package-integrity checks can prove the production module is
|
| 25 |
+
intentional rather than an orphaned source file.
|
| 26 |
|
| 27 |
The package top-level name is `routers` (not szl_*/a11oy_*), so it is intentionally
|
| 28 |
OUTSIDE the guarded-import-liveness first-party scan — and the files exist anyway.
|
|
|
|
| 30 |
Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
|
| 31 |
"""
|
| 32 |
|
| 33 |
+
__all__ = [
|
| 34 |
+
"lambda_bounty",
|
| 35 |
+
"research_3d",
|
| 36 |
+
"frontier_reads",
|
| 37 |
+
"series_a_control_plane",
|
| 38 |
+
]
|
routers/frontier_reads.py
CHANGED
|
@@ -14,7 +14,9 @@ Shared serve.py module-scope state referenced (unchanged, via `import serve`):
|
|
| 14 |
this group, so it moves here with the routes. Registered BEFORE the /api/a11oy/
|
| 15 |
{path:path} Node proxy + SPA catch-all, identical to the pre-refactor inline block.
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
|
| 20 |
"""
|
|
@@ -42,8 +44,7 @@ _A11OY_VERTICALS = [
|
|
| 42 |
|
| 43 |
|
| 44 |
def register(app) -> dict:
|
| 45 |
-
"""Attach
|
| 46 |
-
inline serve.py block. Called BEFORE the Node proxy + SPA catch-all."""
|
| 47 |
import serve # shared module-scope state lives at serve module scope
|
| 48 |
|
| 49 |
@app.get("/api/a11oy/v1/forecast-baseline")
|
|
@@ -60,7 +61,6 @@ def register(app) -> dict:
|
|
| 60 |
"verticals": _A11OY_VERTICALS,
|
| 61 |
"honesty": "Live = shipping pack; stub = scaffolded, roadmap."})
|
| 62 |
|
| 63 |
-
# ---- Business Observability (5 domains) on REAL in-image data (no fabricated KPIs) ----
|
| 64 |
@app.get("/api/a11oy/v1/observability/business")
|
| 65 |
@app.get("/v1/observability/business")
|
| 66 |
async def a11oy_business_observability_v2() -> JSONResponse:
|
|
@@ -89,8 +89,27 @@ def register(app) -> dict:
|
|
| 89 |
"lambda_status": "Conjecture 1 (advisory)",
|
| 90 |
})
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
this group, so it moves here with the routes. Registered BEFORE the /api/a11oy/
|
| 15 |
{path:path} Node proxy + SPA catch-all, identical to the pre-refactor inline block.
|
| 16 |
|
| 17 |
+
The additive Series-A controller is registered at this same pre-catch-all seam. It
|
| 18 |
+
keeps GET/HEAD read-only, uses explicit POSTs for refresh/evaluate/execute, and
|
| 19 |
+
fails one surface closed without taking down the existing frontier reads.
|
| 20 |
|
| 21 |
Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
|
| 22 |
"""
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def register(app) -> dict:
|
| 47 |
+
"""Attach frontier reads and the additive Series-A control plane."""
|
|
|
|
| 48 |
import serve # shared module-scope state lives at serve module scope
|
| 49 |
|
| 50 |
@app.get("/api/a11oy/v1/forecast-baseline")
|
|
|
|
| 61 |
"verticals": _A11OY_VERTICALS,
|
| 62 |
"honesty": "Live = shipping pack; stub = scaffolded, roadmap."})
|
| 63 |
|
|
|
|
| 64 |
@app.get("/api/a11oy/v1/observability/business")
|
| 65 |
@app.get("/v1/observability/business")
|
| 66 |
async def a11oy_business_observability_v2() -> JSONResponse:
|
|
|
|
| 89 |
"lambda_status": "Conjecture 1 (advisory)",
|
| 90 |
})
|
| 91 |
|
| 92 |
+
try:
|
| 93 |
+
from routers import series_a_control_plane as _series_a_control_plane
|
| 94 |
+
|
| 95 |
+
series_a = _series_a_control_plane.register(app, ns="a11oy")
|
| 96 |
+
except Exception as exc: # one additive surface must never take down A11oy
|
| 97 |
+
series_a = {
|
| 98 |
+
"ok": False,
|
| 99 |
+
"state": "UNAVAILABLE",
|
| 100 |
+
"reason": type(exc).__name__,
|
| 101 |
+
"effectors": [],
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
"ok": True,
|
| 106 |
+
"ns": "a11oy",
|
| 107 |
+
"group": "frontier-reads",
|
| 108 |
+
"series_a": series_a,
|
| 109 |
+
"routes": [
|
| 110 |
+
"/api/a11oy/v1/forecast-baseline", "/v1/forecast-baseline",
|
| 111 |
+
"/api/a11oy/v1/vertical-packs", "/v1/vertical-packs",
|
| 112 |
+
"/api/a11oy/v1/observability/business", "/v1/observability/business",
|
| 113 |
+
"/series-a", "/api/a11oy/v1/series-a/status",
|
| 114 |
+
],
|
| 115 |
+
}
|
routers/series_a_control_plane.py
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# © 2026 Lutar, Stephen P. — SZL Holdings
|
| 3 |
+
"""Series-A Live Control Plane for A11oy.
|
| 4 |
+
|
| 5 |
+
One additive controller combines three previously separate payload families:
|
| 6 |
+
|
| 7 |
+
* a signed, current estate truth plane;
|
| 8 |
+
* a bounded Counterfactual Action Passport; and
|
| 9 |
+
* a zero-bandaid, one-attempt local action executor.
|
| 10 |
+
|
| 11 |
+
It uses real GitHub, Hugging Face, HTTP, SQLite, and ECDSA-P256 boundaries.
|
| 12 |
+
GET/HEAD requests never sign or mutate state. Refresh/evaluate/execute operations
|
| 13 |
+
are explicit POSTs, append hash-linked receipts, and fail closed.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import asyncio
|
| 18 |
+
import base64
|
| 19 |
+
import hashlib
|
| 20 |
+
import json
|
| 21 |
+
import os
|
| 22 |
+
import sqlite3
|
| 23 |
+
import threading
|
| 24 |
+
import time
|
| 25 |
+
import uuid
|
| 26 |
+
from dataclasses import dataclass
|
| 27 |
+
from datetime import datetime, timedelta, timezone
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Any, AsyncIterator, Callable, Mapping
|
| 30 |
+
from urllib.parse import urlsplit
|
| 31 |
+
|
| 32 |
+
import httpx
|
| 33 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 34 |
+
from fastapi.responses import HTMLResponse, JSONResponse, Response, StreamingResponse
|
| 35 |
+
|
| 36 |
+
SCHEMA_MANIFEST = "szl.estate-manifest/v2"
|
| 37 |
+
SCHEMA_PASSPORT = "szl.counterfactual-action-passport/v3"
|
| 38 |
+
SCHEMA_RECEIPT = "szl.series-a-receipt/v1"
|
| 39 |
+
SCHEMA_STATUS = "szl.series-a-status/v1"
|
| 40 |
+
SCHEMA_TRUST = "szl.agent-trust-factor/v1"
|
| 41 |
+
PAYLOAD_TYPE = "application/vnd.szl.series-a-receipt.v1+json"
|
| 42 |
+
ORG = "szl-holdings"
|
| 43 |
+
HF_ORG = "SZLHOLDINGS"
|
| 44 |
+
CANONICAL_SPACE = f"{HF_ORG}/a11oy"
|
| 45 |
+
FORBIDDEN_CLONES = tuple(f"{HF_ORG}/a11oy-clone-{index}" for index in range(1, 5))
|
| 46 |
+
TTL_SECONDS = 300
|
| 47 |
+
MAX_BODY = 64 * 1024
|
| 48 |
+
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
|
| 49 |
+
MAX_PAGES = 20
|
| 50 |
+
ALLOWED_ACTIONS = {"estate.refresh", "probe.public_surface"}
|
| 51 |
+
ALLOWED_PROBE_HOSTS = {
|
| 52 |
+
"a-11-oy.com",
|
| 53 |
+
"a11oy.net",
|
| 54 |
+
"szlholdings-a11oy.hf.space",
|
| 55 |
+
"szlholdings-killinchu.hf.space",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _now() -> str:
|
| 60 |
+
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _future(seconds: int) -> str:
|
| 64 |
+
return (datetime.now(timezone.utc) + timedelta(seconds=seconds)).isoformat(
|
| 65 |
+
timespec="milliseconds"
|
| 66 |
+
).replace("+00:00", "Z")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _canonical(value: Any) -> bytes:
|
| 70 |
+
"""Narrow deterministic JSON for signed control-plane records."""
|
| 71 |
+
|
| 72 |
+
def walk(item: Any, path: str = "$") -> None:
|
| 73 |
+
if isinstance(item, float):
|
| 74 |
+
raise ValueError(f"{path}: floats are forbidden in signed records")
|
| 75 |
+
if isinstance(item, dict):
|
| 76 |
+
for key, child in item.items():
|
| 77 |
+
if not isinstance(key, str):
|
| 78 |
+
raise ValueError(f"{path}: keys must be strings")
|
| 79 |
+
lowered = key.lower()
|
| 80 |
+
if any(token in lowered for token in ("password", "secret_value", "private_key", "authorization")):
|
| 81 |
+
raise ValueError(f"{path}.{key}: secret-shaped field is forbidden")
|
| 82 |
+
walk(child, f"{path}.{key}")
|
| 83 |
+
return
|
| 84 |
+
if isinstance(item, list):
|
| 85 |
+
for index, child in enumerate(item):
|
| 86 |
+
walk(child, f"{path}[{index}]")
|
| 87 |
+
return
|
| 88 |
+
if item is None or isinstance(item, (str, int, bool)):
|
| 89 |
+
return
|
| 90 |
+
raise ValueError(f"{path}: unsupported type {type(item).__name__}")
|
| 91 |
+
|
| 92 |
+
walk(value)
|
| 93 |
+
return json.dumps(
|
| 94 |
+
value,
|
| 95 |
+
ensure_ascii=False,
|
| 96 |
+
sort_keys=True,
|
| 97 |
+
separators=(",", ":"),
|
| 98 |
+
allow_nan=False,
|
| 99 |
+
).encode("utf-8")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _sha(value: Any) -> str:
|
| 103 |
+
payload = value if isinstance(value, (bytes, bytearray)) else _canonical(value)
|
| 104 |
+
return hashlib.sha256(payload).hexdigest()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _pae(payload_type: str, payload: bytes) -> bytes:
|
| 108 |
+
ptype = payload_type.encode("utf-8")
|
| 109 |
+
return b"DSSEv1 " + str(len(ptype)).encode() + b" " + ptype + b" " + str(len(payload)).encode() + b" " + payload
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _safe_error(exc: Exception) -> dict[str, str]:
|
| 113 |
+
return {"error_class": type(exc).__name__, "error": str(exc)[:240]}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _git_revision() -> str:
|
| 117 |
+
for key in ("SZL_GIT_SHA", "A11OY_GIT_SHA", "GITHUB_SHA"):
|
| 118 |
+
value = (os.environ.get(key) or "").strip().lower()
|
| 119 |
+
if len(value) == 40 and all(ch in "0123456789abcdef" for ch in value):
|
| 120 |
+
return value
|
| 121 |
+
return "UNKNOWN"
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class ReceiptSigner:
|
| 125 |
+
def __init__(self) -> None:
|
| 126 |
+
self.private_key = None
|
| 127 |
+
self.public_pem = ""
|
| 128 |
+
self.source = "unavailable"
|
| 129 |
+
self.error = ""
|
| 130 |
+
try:
|
| 131 |
+
from a11oy_signing_key import load_signing_key
|
| 132 |
+
|
| 133 |
+
private_key, public_pem, source, error = load_signing_key()
|
| 134 |
+
self.private_key = private_key
|
| 135 |
+
self.public_pem = public_pem or ""
|
| 136 |
+
self.source = source or "unavailable"
|
| 137 |
+
self.error = error or ""
|
| 138 |
+
except Exception as exc:
|
| 139 |
+
self.error = f"{type(exc).__name__}: {str(exc)[:180]}"
|
| 140 |
+
|
| 141 |
+
@property
|
| 142 |
+
def keyid(self) -> str | None:
|
| 143 |
+
return _sha(self.public_pem.encode("utf-8")) if self.public_pem else None
|
| 144 |
+
|
| 145 |
+
def sign(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
| 146 |
+
body = _canonical(dict(payload))
|
| 147 |
+
envelope: dict[str, Any] = {
|
| 148 |
+
"payloadType": PAYLOAD_TYPE,
|
| 149 |
+
"payload": base64.b64encode(body).decode("ascii"),
|
| 150 |
+
"signatures": [],
|
| 151 |
+
"pae_sha256": hashlib.sha256(_pae(PAYLOAD_TYPE, body)).hexdigest(),
|
| 152 |
+
"key_source": self.source,
|
| 153 |
+
}
|
| 154 |
+
if self.private_key is None:
|
| 155 |
+
envelope["signature_status"] = "UNSIGNED_UNAVAILABLE"
|
| 156 |
+
envelope["signature_error"] = self.error or "signing key unavailable"
|
| 157 |
+
return envelope
|
| 158 |
+
try:
|
| 159 |
+
from cryptography.hazmat.primitives import hashes
|
| 160 |
+
from cryptography.hazmat.primitives.asymmetric import ec
|
| 161 |
+
|
| 162 |
+
signature = self.private_key.sign(
|
| 163 |
+
_pae(PAYLOAD_TYPE, body), ec.ECDSA(hashes.SHA256())
|
| 164 |
+
)
|
| 165 |
+
envelope["signatures"] = [
|
| 166 |
+
{
|
| 167 |
+
"keyid": self.keyid,
|
| 168 |
+
"sig": base64.b64encode(signature).decode("ascii"),
|
| 169 |
+
}
|
| 170 |
+
]
|
| 171 |
+
envelope["signature_status"] = "SIGNED"
|
| 172 |
+
return envelope
|
| 173 |
+
except Exception as exc:
|
| 174 |
+
envelope["signature_status"] = "UNSIGNED_ERROR"
|
| 175 |
+
envelope["signature_error"] = f"{type(exc).__name__}: {str(exc)[:180]}"
|
| 176 |
+
return envelope
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class Store:
|
| 180 |
+
def __init__(self, requested_path: str | None = None) -> None:
|
| 181 |
+
self.path = self._resolve_path(requested_path)
|
| 182 |
+
self.lock = threading.RLock()
|
| 183 |
+
self._init()
|
| 184 |
+
|
| 185 |
+
@staticmethod
|
| 186 |
+
def _resolve_path(requested: str | None) -> str:
|
| 187 |
+
candidates = [
|
| 188 |
+
requested or os.environ.get("A11OY_SERIES_A_DB") or "/data/series-a/control-plane.sqlite3",
|
| 189 |
+
"/tmp/a11oy_series_a_control_plane.sqlite3",
|
| 190 |
+
]
|
| 191 |
+
for candidate in candidates:
|
| 192 |
+
try:
|
| 193 |
+
path = Path(candidate)
|
| 194 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 195 |
+
with path.parent.joinpath(".write-probe").open("w", encoding="utf-8") as probe:
|
| 196 |
+
probe.write("ok")
|
| 197 |
+
path.parent.joinpath(".write-probe").unlink(missing_ok=True)
|
| 198 |
+
return str(path)
|
| 199 |
+
except Exception:
|
| 200 |
+
continue
|
| 201 |
+
raise RuntimeError("no writable SQLite location")
|
| 202 |
+
|
| 203 |
+
def connect(self) -> sqlite3.Connection:
|
| 204 |
+
connection = sqlite3.connect(self.path, timeout=30)
|
| 205 |
+
connection.row_factory = sqlite3.Row
|
| 206 |
+
connection.execute("PRAGMA foreign_keys=ON")
|
| 207 |
+
connection.execute("PRAGMA journal_mode=WAL")
|
| 208 |
+
connection.execute("PRAGMA synchronous=FULL")
|
| 209 |
+
return connection
|
| 210 |
+
|
| 211 |
+
def _init(self) -> None:
|
| 212 |
+
with self.lock, self.connect() as db:
|
| 213 |
+
db.executescript(
|
| 214 |
+
"""
|
| 215 |
+
CREATE TABLE IF NOT EXISTS snapshots(
|
| 216 |
+
digest TEXT PRIMARY KEY,
|
| 217 |
+
payload TEXT NOT NULL,
|
| 218 |
+
envelope TEXT NOT NULL,
|
| 219 |
+
observed_at TEXT NOT NULL,
|
| 220 |
+
valid_until TEXT NOT NULL
|
| 221 |
+
);
|
| 222 |
+
CREATE TABLE IF NOT EXISTS passports(
|
| 223 |
+
digest TEXT PRIMARY KEY,
|
| 224 |
+
payload TEXT NOT NULL,
|
| 225 |
+
decision TEXT NOT NULL,
|
| 226 |
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK(attempts BETWEEN 0 AND 1),
|
| 227 |
+
created_at TEXT NOT NULL
|
| 228 |
+
);
|
| 229 |
+
CREATE TABLE IF NOT EXISTS receipts(
|
| 230 |
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 231 |
+
receipt_id TEXT NOT NULL UNIQUE,
|
| 232 |
+
kind TEXT NOT NULL,
|
| 233 |
+
payload TEXT NOT NULL,
|
| 234 |
+
envelope TEXT NOT NULL,
|
| 235 |
+
previous_hash TEXT NOT NULL,
|
| 236 |
+
receipt_hash TEXT NOT NULL UNIQUE,
|
| 237 |
+
created_at TEXT NOT NULL
|
| 238 |
+
);
|
| 239 |
+
CREATE TABLE IF NOT EXISTS events(
|
| 240 |
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 241 |
+
event_id TEXT NOT NULL UNIQUE,
|
| 242 |
+
kind TEXT NOT NULL,
|
| 243 |
+
payload TEXT NOT NULL,
|
| 244 |
+
created_at TEXT NOT NULL
|
| 245 |
+
);
|
| 246 |
+
"""
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
def append_event(self, kind: str, payload: Mapping[str, Any]) -> None:
|
| 250 |
+
with self.lock, self.connect() as db:
|
| 251 |
+
db.execute(
|
| 252 |
+
"INSERT INTO events(event_id,kind,payload,created_at) VALUES(?,?,?,?)",
|
| 253 |
+
(f"evt_{uuid.uuid4().hex}", kind, json.dumps(dict(payload), sort_keys=True), _now()),
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
def events_since(self, sequence: int, limit: int = 100) -> list[dict[str, Any]]:
|
| 257 |
+
with self.lock, self.connect() as db:
|
| 258 |
+
rows = db.execute(
|
| 259 |
+
"SELECT sequence,event_id,kind,payload,created_at FROM events WHERE sequence>? ORDER BY sequence LIMIT ?",
|
| 260 |
+
(max(0, sequence), max(1, min(limit, 500))),
|
| 261 |
+
).fetchall()
|
| 262 |
+
return [
|
| 263 |
+
{
|
| 264 |
+
"sequence": row["sequence"],
|
| 265 |
+
"event_id": row["event_id"],
|
| 266 |
+
"kind": row["kind"],
|
| 267 |
+
"payload": json.loads(row["payload"]),
|
| 268 |
+
"created_at": row["created_at"],
|
| 269 |
+
}
|
| 270 |
+
for row in rows
|
| 271 |
+
]
|
| 272 |
+
|
| 273 |
+
def append_receipt(
|
| 274 |
+
self, kind: str, payload: Mapping[str, Any], signer: ReceiptSigner
|
| 275 |
+
) -> dict[str, Any]:
|
| 276 |
+
with self.lock, self.connect() as db:
|
| 277 |
+
row = db.execute(
|
| 278 |
+
"SELECT receipt_hash FROM receipts ORDER BY sequence DESC LIMIT 1"
|
| 279 |
+
).fetchone()
|
| 280 |
+
previous = row["receipt_hash"] if row else "0" * 64
|
| 281 |
+
receipt = {
|
| 282 |
+
"schema": SCHEMA_RECEIPT,
|
| 283 |
+
"receipt_id": f"rcpt_{uuid.uuid4().hex}",
|
| 284 |
+
"kind": kind,
|
| 285 |
+
"created_at": _now(),
|
| 286 |
+
"source_revision": _git_revision(),
|
| 287 |
+
"previous_receipt_hash": previous,
|
| 288 |
+
"payload": dict(payload),
|
| 289 |
+
}
|
| 290 |
+
envelope = signer.sign(receipt)
|
| 291 |
+
receipt_hash = _sha(envelope)
|
| 292 |
+
db.execute(
|
| 293 |
+
"""INSERT INTO receipts(receipt_id,kind,payload,envelope,previous_hash,receipt_hash,created_at)
|
| 294 |
+
VALUES(?,?,?,?,?,?,?)""",
|
| 295 |
+
(
|
| 296 |
+
receipt["receipt_id"],
|
| 297 |
+
kind,
|
| 298 |
+
json.dumps(receipt, sort_keys=True),
|
| 299 |
+
json.dumps(envelope, sort_keys=True),
|
| 300 |
+
previous,
|
| 301 |
+
receipt_hash,
|
| 302 |
+
receipt["created_at"],
|
| 303 |
+
),
|
| 304 |
+
)
|
| 305 |
+
self.append_event(kind, {"receipt_hash": receipt_hash, "receipt_id": receipt["receipt_id"]})
|
| 306 |
+
return {"receipt": receipt, "envelope": envelope, "receipt_hash": receipt_hash}
|
| 307 |
+
|
| 308 |
+
def list_receipts(self, limit: int = 50) -> list[dict[str, Any]]:
|
| 309 |
+
with self.lock, self.connect() as db:
|
| 310 |
+
rows = db.execute(
|
| 311 |
+
"SELECT sequence,kind,payload,envelope,receipt_hash,created_at FROM receipts ORDER BY sequence DESC LIMIT ?",
|
| 312 |
+
(max(1, min(limit, 200)),),
|
| 313 |
+
).fetchall()
|
| 314 |
+
return [
|
| 315 |
+
{
|
| 316 |
+
"sequence": row["sequence"],
|
| 317 |
+
"kind": row["kind"],
|
| 318 |
+
"receipt": json.loads(row["payload"]),
|
| 319 |
+
"envelope": json.loads(row["envelope"]),
|
| 320 |
+
"receipt_hash": row["receipt_hash"],
|
| 321 |
+
"created_at": row["created_at"],
|
| 322 |
+
}
|
| 323 |
+
for row in rows
|
| 324 |
+
]
|
| 325 |
+
|
| 326 |
+
def save_snapshot(self, manifest: Mapping[str, Any], envelope: Mapping[str, Any]) -> str:
|
| 327 |
+
digest = _sha(manifest)
|
| 328 |
+
with self.lock, self.connect() as db:
|
| 329 |
+
db.execute(
|
| 330 |
+
"INSERT OR REPLACE INTO snapshots(digest,payload,envelope,observed_at,valid_until) VALUES(?,?,?,?,?)",
|
| 331 |
+
(
|
| 332 |
+
digest,
|
| 333 |
+
json.dumps(dict(manifest), sort_keys=True),
|
| 334 |
+
json.dumps(dict(envelope), sort_keys=True),
|
| 335 |
+
manifest["observed_at"],
|
| 336 |
+
manifest["valid_until"],
|
| 337 |
+
),
|
| 338 |
+
)
|
| 339 |
+
return digest
|
| 340 |
+
|
| 341 |
+
def latest_snapshot(self) -> dict[str, Any] | None:
|
| 342 |
+
with self.lock, self.connect() as db:
|
| 343 |
+
row = db.execute(
|
| 344 |
+
"SELECT digest,payload,envelope,observed_at,valid_until FROM snapshots ORDER BY observed_at DESC LIMIT 1"
|
| 345 |
+
).fetchone()
|
| 346 |
+
if row is None:
|
| 347 |
+
return None
|
| 348 |
+
return {
|
| 349 |
+
"digest": row["digest"],
|
| 350 |
+
"manifest": json.loads(row["payload"]),
|
| 351 |
+
"envelope": json.loads(row["envelope"]),
|
| 352 |
+
"observed_at": row["observed_at"],
|
| 353 |
+
"valid_until": row["valid_until"],
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
def save_passport(self, passport: Mapping[str, Any]) -> str:
|
| 357 |
+
digest = _sha(passport)
|
| 358 |
+
with self.lock, self.connect() as db:
|
| 359 |
+
db.execute(
|
| 360 |
+
"INSERT INTO passports(digest,payload,decision,attempts,created_at) VALUES(?,?,?,?,?)",
|
| 361 |
+
(digest, json.dumps(dict(passport), sort_keys=True), passport["decision"], 0, passport["created_at"]),
|
| 362 |
+
)
|
| 363 |
+
return digest
|
| 364 |
+
|
| 365 |
+
def load_passport(self, digest: str) -> dict[str, Any] | None:
|
| 366 |
+
with self.lock, self.connect() as db:
|
| 367 |
+
row = db.execute(
|
| 368 |
+
"SELECT payload,decision,attempts FROM passports WHERE digest=?", (digest,)
|
| 369 |
+
).fetchone()
|
| 370 |
+
if row is None:
|
| 371 |
+
return None
|
| 372 |
+
value = json.loads(row["payload"])
|
| 373 |
+
value["attempts"] = row["attempts"]
|
| 374 |
+
return value
|
| 375 |
+
|
| 376 |
+
def consume_attempt(self, digest: str) -> None:
|
| 377 |
+
with self.lock, self.connect() as db:
|
| 378 |
+
result = db.execute(
|
| 379 |
+
"UPDATE passports SET attempts=1 WHERE digest=? AND attempts=0", (digest,)
|
| 380 |
+
)
|
| 381 |
+
if result.rowcount != 1:
|
| 382 |
+
raise RuntimeError("passport attempt is absent or already consumed")
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
@dataclass
|
| 386 |
+
class Observation:
|
| 387 |
+
state: str
|
| 388 |
+
value: Any = None
|
| 389 |
+
detail: Mapping[str, Any] | None = None
|
| 390 |
+
|
| 391 |
+
def as_dict(self) -> dict[str, Any]:
|
| 392 |
+
value = {"state": self.state}
|
| 393 |
+
if self.value is not None:
|
| 394 |
+
value["value"] = self.value
|
| 395 |
+
if self.detail:
|
| 396 |
+
value["detail"] = dict(self.detail)
|
| 397 |
+
return value
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
class Collector:
|
| 401 |
+
def __init__(self) -> None:
|
| 402 |
+
self.github_token = (os.environ.get("GITHUB_TOKEN") or "").strip()
|
| 403 |
+
self.hf_token = (os.environ.get("HF_TOKEN") or "").strip()
|
| 404 |
+
|
| 405 |
+
async def _json(
|
| 406 |
+
self,
|
| 407 |
+
client: httpx.AsyncClient,
|
| 408 |
+
url: str,
|
| 409 |
+
*,
|
| 410 |
+
params: Mapping[str, Any] | None = None,
|
| 411 |
+
allowed_host: str,
|
| 412 |
+
) -> tuple[Any, httpx.Response]:
|
| 413 |
+
parsed = urlsplit(url)
|
| 414 |
+
if parsed.scheme != "https" or parsed.hostname != allowed_host or parsed.username or parsed.password:
|
| 415 |
+
raise RuntimeError("outbound URL left the fixed HTTPS origin")
|
| 416 |
+
response = await client.get(url, params=params)
|
| 417 |
+
if response.status_code != 200:
|
| 418 |
+
raise RuntimeError(f"HTTP {response.status_code}")
|
| 419 |
+
if len(response.content) > MAX_RESPONSE_BYTES:
|
| 420 |
+
raise RuntimeError("response exceeded byte limit")
|
| 421 |
+
final = urlsplit(str(response.url))
|
| 422 |
+
if final.scheme != "https" or final.hostname != allowed_host:
|
| 423 |
+
raise RuntimeError("redirect left the fixed HTTPS origin")
|
| 424 |
+
return response.json(), response
|
| 425 |
+
|
| 426 |
+
async def github(self) -> Observation:
|
| 427 |
+
headers = {"accept": "application/vnd.github+json", "user-agent": "szl-series-a/1"}
|
| 428 |
+
if self.github_token:
|
| 429 |
+
headers["authorization"] = f"Bearer {self.github_token}"
|
| 430 |
+
try:
|
| 431 |
+
repos: list[dict[str, Any]] = []
|
| 432 |
+
async with httpx.AsyncClient(headers=headers, timeout=30, follow_redirects=False) as client:
|
| 433 |
+
complete = False
|
| 434 |
+
for page in range(1, MAX_PAGES + 1):
|
| 435 |
+
values, _ = await self._json(
|
| 436 |
+
client,
|
| 437 |
+
f"https://api.github.com/orgs/{ORG}/repos",
|
| 438 |
+
params={"type": "all", "per_page": 100, "page": page},
|
| 439 |
+
allowed_host="api.github.com",
|
| 440 |
+
)
|
| 441 |
+
if not isinstance(values, list):
|
| 442 |
+
raise RuntimeError("repository listing was not an array")
|
| 443 |
+
repos.extend(item for item in values if isinstance(item, dict))
|
| 444 |
+
if len(values) < 100:
|
| 445 |
+
complete = True
|
| 446 |
+
break
|
| 447 |
+
if not complete:
|
| 448 |
+
raise RuntimeError("repository pagination exceeded bounded window")
|
| 449 |
+
pr_data, _ = await self._json(
|
| 450 |
+
client,
|
| 451 |
+
"https://api.github.com/search/issues",
|
| 452 |
+
params={"q": f"org:{ORG} is:pr is:open", "per_page": 1},
|
| 453 |
+
allowed_host="api.github.com",
|
| 454 |
+
)
|
| 455 |
+
rows = [
|
| 456 |
+
{
|
| 457 |
+
"name": str(item.get("name") or ""),
|
| 458 |
+
"archived": bool(item.get("archived")),
|
| 459 |
+
"visibility": str(item.get("visibility") or "unknown"),
|
| 460 |
+
"default_branch": str(item.get("default_branch") or ""),
|
| 461 |
+
"updated_at": str(item.get("updated_at") or ""),
|
| 462 |
+
}
|
| 463 |
+
for item in repos
|
| 464 |
+
]
|
| 465 |
+
return Observation(
|
| 466 |
+
"OBSERVED",
|
| 467 |
+
{
|
| 468 |
+
"repository_count": len(rows),
|
| 469 |
+
"open_pull_request_count": int((pr_data or {}).get("total_count", 0)),
|
| 470 |
+
"pagination_complete": True,
|
| 471 |
+
"repositories": rows,
|
| 472 |
+
},
|
| 473 |
+
{"authenticated": bool(self.github_token)},
|
| 474 |
+
)
|
| 475 |
+
except Exception as exc:
|
| 476 |
+
return Observation("UNAVAILABLE", detail=_safe_error(exc))
|
| 477 |
+
|
| 478 |
+
def _hf_list(self, method_name: str, kwargs: Mapping[str, Any]) -> list[Any]:
|
| 479 |
+
from huggingface_hub import HfApi
|
| 480 |
+
|
| 481 |
+
api = HfApi(token=self.hf_token or None)
|
| 482 |
+
method = getattr(api, method_name, None)
|
| 483 |
+
if method is None:
|
| 484 |
+
raise AttributeError(f"HfApi.{method_name} unavailable")
|
| 485 |
+
return list(method(**dict(kwargs)))
|
| 486 |
+
|
| 487 |
+
async def _hf_kernels(self) -> list[dict[str, Any]]:
|
| 488 |
+
headers = {"accept": "application/json", "user-agent": "szl-series-a/1"}
|
| 489 |
+
if self.hf_token:
|
| 490 |
+
headers["authorization"] = f"Bearer {self.hf_token}"
|
| 491 |
+
output: list[dict[str, Any]] = []
|
| 492 |
+
url: str | None = "https://huggingface.co/api/kernels"
|
| 493 |
+
params: Mapping[str, Any] | None = {"author": HF_ORG, "limit": 1000, "full": "true"}
|
| 494 |
+
async with httpx.AsyncClient(headers=headers, timeout=30, follow_redirects=False) as client:
|
| 495 |
+
for _ in range(MAX_PAGES):
|
| 496 |
+
if not url:
|
| 497 |
+
return output
|
| 498 |
+
values, response = await self._json(
|
| 499 |
+
client, url, params=params, allowed_host="huggingface.co"
|
| 500 |
+
)
|
| 501 |
+
if not isinstance(values, list):
|
| 502 |
+
raise RuntimeError("kernel listing was not an array")
|
| 503 |
+
output.extend(item for item in values if isinstance(item, dict))
|
| 504 |
+
link = response.links.get("next") or {}
|
| 505 |
+
url = link.get("url") if isinstance(link, dict) else None
|
| 506 |
+
params = None
|
| 507 |
+
if not url:
|
| 508 |
+
return output
|
| 509 |
+
raise RuntimeError("kernel pagination exceeded bounded window")
|
| 510 |
+
|
| 511 |
+
async def huggingface(self) -> Observation:
|
| 512 |
+
categories: dict[str, Any] = {}
|
| 513 |
+
errors: dict[str, Any] = {}
|
| 514 |
+
methods = {
|
| 515 |
+
"models": ("list_models", {"author": HF_ORG}),
|
| 516 |
+
"datasets": ("list_datasets", {"author": HF_ORG}),
|
| 517 |
+
"spaces": ("list_spaces", {"author": HF_ORG}),
|
| 518 |
+
"collections": ("list_collections", {"owner": HF_ORG}),
|
| 519 |
+
"buckets": ("list_buckets", {"namespace": HF_ORG}),
|
| 520 |
+
}
|
| 521 |
+
for name, (method, kwargs) in methods.items():
|
| 522 |
+
try:
|
| 523 |
+
items = await asyncio.to_thread(self._hf_list, method, kwargs)
|
| 524 |
+
rows = []
|
| 525 |
+
for item in items:
|
| 526 |
+
item_id = None
|
| 527 |
+
for field in ("id", "repo_id", "name", "slug"):
|
| 528 |
+
candidate = item.get(field) if isinstance(item, dict) else getattr(item, field, None)
|
| 529 |
+
if isinstance(candidate, str) and candidate:
|
| 530 |
+
item_id = candidate
|
| 531 |
+
break
|
| 532 |
+
rows.append({"id": item_id})
|
| 533 |
+
categories[name] = {"state": "OBSERVED", "count": len(rows), "items": rows}
|
| 534 |
+
except Exception as exc:
|
| 535 |
+
categories[name] = {"state": "UNAVAILABLE"}
|
| 536 |
+
errors[name] = _safe_error(exc)
|
| 537 |
+
try:
|
| 538 |
+
kernels = await self._hf_kernels()
|
| 539 |
+
categories["kernels"] = {
|
| 540 |
+
"state": "OBSERVED",
|
| 541 |
+
"count": len(kernels),
|
| 542 |
+
"items": [{"id": str(item.get("id") or item.get("repo_id") or "")} for item in kernels],
|
| 543 |
+
}
|
| 544 |
+
except Exception as exc:
|
| 545 |
+
categories["kernels"] = {"state": "UNAVAILABLE"}
|
| 546 |
+
errors["kernels"] = _safe_error(exc)
|
| 547 |
+
|
| 548 |
+
space_ids = {
|
| 549 |
+
row.get("id")
|
| 550 |
+
for row in categories.get("spaces", {}).get("items", [])
|
| 551 |
+
if isinstance(row, dict)
|
| 552 |
+
}
|
| 553 |
+
clones_present = sorted(value for value in FORBIDDEN_CLONES if value in space_ids)
|
| 554 |
+
canonical_present = CANONICAL_SPACE in space_ids
|
| 555 |
+
state = "OBSERVED" if categories.get("spaces", {}).get("state") == "OBSERVED" else "PARTIAL"
|
| 556 |
+
return Observation(
|
| 557 |
+
state,
|
| 558 |
+
{
|
| 559 |
+
"categories": categories,
|
| 560 |
+
"canonical_space": CANONICAL_SPACE,
|
| 561 |
+
"canonical_present": canonical_present,
|
| 562 |
+
"forbidden_clones_present": clones_present,
|
| 563 |
+
"singleton_ok": canonical_present and not clones_present,
|
| 564 |
+
},
|
| 565 |
+
{"authenticated": bool(self.hf_token), "errors": errors},
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
async def collect(self) -> dict[str, Any]:
|
| 569 |
+
github, hf = await asyncio.gather(self.github(), self.huggingface())
|
| 570 |
+
critical_failures: list[str] = []
|
| 571 |
+
if github.state != "OBSERVED":
|
| 572 |
+
critical_failures.append("github_inventory_unavailable")
|
| 573 |
+
if hf.state not in {"OBSERVED", "PARTIAL"}:
|
| 574 |
+
critical_failures.append("huggingface_inventory_unavailable")
|
| 575 |
+
hf_value = hf.value if isinstance(hf.value, dict) else {}
|
| 576 |
+
if hf_value and not hf_value.get("singleton_ok"):
|
| 577 |
+
critical_failures.append("canonical_a11oy_singleton_failed")
|
| 578 |
+
categories = hf_value.get("categories", {}) if isinstance(hf_value, dict) else {}
|
| 579 |
+
counts = {
|
| 580 |
+
name: value.get("count") if isinstance(value, dict) and value.get("state") == "OBSERVED" else None
|
| 581 |
+
for name, value in categories.items()
|
| 582 |
+
}
|
| 583 |
+
manifest = {
|
| 584 |
+
"schema": SCHEMA_MANIFEST,
|
| 585 |
+
"observed_at": _now(),
|
| 586 |
+
"valid_until": _future(TTL_SECONDS),
|
| 587 |
+
"source_revision": _git_revision(),
|
| 588 |
+
"organization": ORG,
|
| 589 |
+
"huggingface_organization": HF_ORG,
|
| 590 |
+
"status": "BLOCKED" if critical_failures else "OBSERVED",
|
| 591 |
+
"critical_failures": critical_failures,
|
| 592 |
+
"github": github.as_dict(),
|
| 593 |
+
"huggingface": hf.as_dict(),
|
| 594 |
+
"counts": {
|
| 595 |
+
"github_repositories": (
|
| 596 |
+
github.value.get("repository_count")
|
| 597 |
+
if isinstance(github.value, dict) and github.state == "OBSERVED"
|
| 598 |
+
else None
|
| 599 |
+
),
|
| 600 |
+
"github_open_pull_requests": (
|
| 601 |
+
github.value.get("open_pull_request_count")
|
| 602 |
+
if isinstance(github.value, dict) and github.state == "OBSERVED"
|
| 603 |
+
else None
|
| 604 |
+
),
|
| 605 |
+
**counts,
|
| 606 |
+
},
|
| 607 |
+
"claim": "CURRENT_OBSERVATION_NOT_ETERNAL_TRUTH",
|
| 608 |
+
"counterfactual_label": "MODELED",
|
| 609 |
+
"private_reasoning_collected": False,
|
| 610 |
+
}
|
| 611 |
+
manifest["manifest_digest"] = _sha(manifest)
|
| 612 |
+
return manifest
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
class Service:
|
| 616 |
+
def __init__(self, db_path: str | None = None) -> None:
|
| 617 |
+
self.store = Store(db_path)
|
| 618 |
+
self.signer = ReceiptSigner()
|
| 619 |
+
self.collector = Collector()
|
| 620 |
+
self.refresh_lock = asyncio.Lock()
|
| 621 |
+
self.started = False
|
| 622 |
+
self.background_task: asyncio.Task[Any] | None = None
|
| 623 |
+
|
| 624 |
+
async def start(self) -> None:
|
| 625 |
+
if self.started:
|
| 626 |
+
return
|
| 627 |
+
self.started = True
|
| 628 |
+
if (os.environ.get("A11OY_SERIES_A_STARTUP_REFRESH") or "1").strip() == "0":
|
| 629 |
+
self.store.append_event("estate.refresh.skipped", {"reason": "explicit test/runtime configuration"})
|
| 630 |
+
return
|
| 631 |
+
|
| 632 |
+
async def run() -> None:
|
| 633 |
+
try:
|
| 634 |
+
await self.refresh("startup")
|
| 635 |
+
except Exception as exc:
|
| 636 |
+
self.store.append_event("estate.refresh.failed", _safe_error(exc))
|
| 637 |
+
|
| 638 |
+
self.background_task = asyncio.create_task(run(), name="a11oy-series-a-startup-refresh")
|
| 639 |
+
|
| 640 |
+
async def refresh(self, actor: str) -> dict[str, Any]:
|
| 641 |
+
if self.refresh_lock.locked():
|
| 642 |
+
raise HTTPException(status_code=409, detail="estate refresh already running")
|
| 643 |
+
async with self.refresh_lock:
|
| 644 |
+
manifest = await self.collector.collect()
|
| 645 |
+
envelope = self.signer.sign(manifest)
|
| 646 |
+
digest = self.store.save_snapshot(manifest, envelope)
|
| 647 |
+
receipt = self.store.append_receipt(
|
| 648 |
+
"estate.refresh",
|
| 649 |
+
{
|
| 650 |
+
"actor": actor,
|
| 651 |
+
"manifest_digest": digest,
|
| 652 |
+
"status": manifest["status"],
|
| 653 |
+
"counts": manifest["counts"],
|
| 654 |
+
},
|
| 655 |
+
self.signer,
|
| 656 |
+
)
|
| 657 |
+
return {"manifest": manifest, "envelope": envelope, "refresh_receipt": receipt}
|
| 658 |
+
|
| 659 |
+
def latest_status(self) -> dict[str, Any]:
|
| 660 |
+
latest = self.store.latest_snapshot()
|
| 661 |
+
if latest is None:
|
| 662 |
+
return {
|
| 663 |
+
"schema": SCHEMA_STATUS,
|
| 664 |
+
"state": "PENDING",
|
| 665 |
+
"terminal": True,
|
| 666 |
+
"source_revision": _git_revision(),
|
| 667 |
+
"signing_key_source": self.signer.source,
|
| 668 |
+
"database": self.store.path,
|
| 669 |
+
"detail": "no completed refresh is persisted yet",
|
| 670 |
+
}
|
| 671 |
+
valid_until = datetime.fromisoformat(latest["valid_until"].replace("Z", "+00:00"))
|
| 672 |
+
stale = datetime.now(timezone.utc) >= valid_until
|
| 673 |
+
manifest = latest["manifest"]
|
| 674 |
+
return {
|
| 675 |
+
"schema": SCHEMA_STATUS,
|
| 676 |
+
"state": "STALE" if stale else manifest["status"],
|
| 677 |
+
"terminal": True,
|
| 678 |
+
"source_revision": _git_revision(),
|
| 679 |
+
"manifest_digest": latest["digest"],
|
| 680 |
+
"observed_at": latest["observed_at"],
|
| 681 |
+
"valid_until": latest["valid_until"],
|
| 682 |
+
"counts": manifest.get("counts", {}),
|
| 683 |
+
"critical_failures": manifest.get("critical_failures", []),
|
| 684 |
+
"signature_status": latest["envelope"].get("signature_status"),
|
| 685 |
+
"signing_key_source": self.signer.source,
|
| 686 |
+
"database": self.store.path,
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
def evaluate_passport(self, body: Mapping[str, Any]) -> dict[str, Any]:
|
| 690 |
+
action = body.get("action")
|
| 691 |
+
if not isinstance(action, dict):
|
| 692 |
+
raise HTTPException(status_code=422, detail="action must be an object")
|
| 693 |
+
action_type = str(action.get("type") or "")
|
| 694 |
+
target = str(action.get("target") or "")
|
| 695 |
+
impact = str(action.get("impact") or "MODERATE").upper()
|
| 696 |
+
irreversible = bool(action.get("irreversible", False))
|
| 697 |
+
if action_type not in ALLOWED_ACTIONS:
|
| 698 |
+
decision = "BLOCK"
|
| 699 |
+
reasons = ["ACTION_TYPE_NOT_ALLOWLISTED"]
|
| 700 |
+
elif not target:
|
| 701 |
+
decision = "BLOCK"
|
| 702 |
+
reasons = ["TARGET_REQUIRED"]
|
| 703 |
+
elif impact in {"HIGH", "CRITICAL"} or irreversible:
|
| 704 |
+
decision = "REQUIRE_APPROVAL"
|
| 705 |
+
reasons = ["INDEPENDENT_APPROVAL_REQUIRED"]
|
| 706 |
+
else:
|
| 707 |
+
decision = "ALLOW"
|
| 708 |
+
reasons = ["BOUNDED_REVERSIBLE_ACTION"]
|
| 709 |
+
|
| 710 |
+
evidence = body.get("evidence")
|
| 711 |
+
if not isinstance(evidence, list) or not evidence:
|
| 712 |
+
decision = "BLOCK"
|
| 713 |
+
reasons = sorted(set(reasons + ["EVIDENCE_REQUIRED"]))
|
| 714 |
+
else:
|
| 715 |
+
for item in evidence:
|
| 716 |
+
if not isinstance(item, dict) or item.get("label") in {"UNKNOWN", "UNAVAILABLE"}:
|
| 717 |
+
decision = "BLOCK"
|
| 718 |
+
reasons = sorted(set(reasons + ["NON_ACTIONABLE_EVIDENCE"]))
|
| 719 |
+
break
|
| 720 |
+
|
| 721 |
+
no_action = {
|
| 722 |
+
"scenario_id": "no-action",
|
| 723 |
+
"kind": "NO_ACTION",
|
| 724 |
+
"label": "MODELED",
|
| 725 |
+
"outcome": str(body.get("expected_if_withheld") or "current state persists"),
|
| 726 |
+
}
|
| 727 |
+
proposed = {
|
| 728 |
+
"scenario_id": "proposed-action",
|
| 729 |
+
"kind": "PROPOSED_ACTION",
|
| 730 |
+
"label": "MODELED",
|
| 731 |
+
"outcome": str(body.get("expected_if_acted") or "bounded action completes or fails closed"),
|
| 732 |
+
}
|
| 733 |
+
passport = {
|
| 734 |
+
"schema": SCHEMA_PASSPORT,
|
| 735 |
+
"passport_id": f"cap_{uuid.uuid4().hex}",
|
| 736 |
+
"created_at": _now(),
|
| 737 |
+
"source_revision": _git_revision(),
|
| 738 |
+
"subject": {
|
| 739 |
+
"principal_id": str(body.get("principal_id") or "anonymous-proposer"),
|
| 740 |
+
"workload_id": str(body.get("workload_id") or "a11oy-series-a"),
|
| 741 |
+
},
|
| 742 |
+
"action": action,
|
| 743 |
+
"action_digest": _sha(action),
|
| 744 |
+
"evidence": evidence if isinstance(evidence, list) else [],
|
| 745 |
+
"counterfactuals": [no_action, proposed],
|
| 746 |
+
"decision": decision,
|
| 747 |
+
"reason_codes": reasons,
|
| 748 |
+
"max_attempts": 1,
|
| 749 |
+
"private_reasoning_collected": False,
|
| 750 |
+
}
|
| 751 |
+
digest = self.store.save_passport(passport)
|
| 752 |
+
receipt = self.store.append_receipt(
|
| 753 |
+
"passport.evaluate",
|
| 754 |
+
{"passport_digest": digest, "decision": decision, "reason_codes": reasons},
|
| 755 |
+
self.signer,
|
| 756 |
+
)
|
| 757 |
+
return {"passport": passport, "passport_digest": digest, "decision_receipt": receipt}
|
| 758 |
+
|
| 759 |
+
async def execute(self, body: Mapping[str, Any]) -> dict[str, Any]:
|
| 760 |
+
digest = str(body.get("passport_digest") or "")
|
| 761 |
+
passport = self.store.load_passport(digest)
|
| 762 |
+
if passport is None:
|
| 763 |
+
raise HTTPException(status_code=404, detail="passport not found")
|
| 764 |
+
if passport["attempts"] != 0:
|
| 765 |
+
raise HTTPException(status_code=409, detail="passport attempt already consumed")
|
| 766 |
+
if passport["decision"] != "ALLOW":
|
| 767 |
+
raise HTTPException(status_code=403, detail=f"passport decision is {passport['decision']}")
|
| 768 |
+
self.store.consume_attempt(digest)
|
| 769 |
+
action = passport["action"]
|
| 770 |
+
started = _now()
|
| 771 |
+
try:
|
| 772 |
+
if action["type"] == "estate.refresh":
|
| 773 |
+
result = await self.refresh(passport["passport_id"])
|
| 774 |
+
outcome = {
|
| 775 |
+
"status": "SUCCEEDED",
|
| 776 |
+
"manifest_digest": result["manifest"]["manifest_digest"],
|
| 777 |
+
"estate_status": result["manifest"]["status"],
|
| 778 |
+
}
|
| 779 |
+
elif action["type"] == "probe.public_surface":
|
| 780 |
+
outcome = await self._probe(str(action["target"]))
|
| 781 |
+
else:
|
| 782 |
+
raise RuntimeError("action left allowlist after authorization")
|
| 783 |
+
except Exception as exc:
|
| 784 |
+
outcome = {"status": "FAILED", **_safe_error(exc)}
|
| 785 |
+
outcome.update(
|
| 786 |
+
{
|
| 787 |
+
"started_at": started,
|
| 788 |
+
"completed_at": _now(),
|
| 789 |
+
"attempt": 1,
|
| 790 |
+
"max_attempts": 1,
|
| 791 |
+
"passport_digest": digest,
|
| 792 |
+
}
|
| 793 |
+
)
|
| 794 |
+
receipt = self.store.append_receipt("passport.outcome", outcome, self.signer)
|
| 795 |
+
return {"outcome": outcome, "outcome_receipt": receipt}
|
| 796 |
+
|
| 797 |
+
async def _probe(self, target: str) -> dict[str, Any]:
|
| 798 |
+
parsed = urlsplit(target)
|
| 799 |
+
if parsed.scheme != "https" or parsed.hostname not in ALLOWED_PROBE_HOSTS or parsed.username or parsed.password:
|
| 800 |
+
raise RuntimeError("probe target is not in the fixed HTTPS allowlist")
|
| 801 |
+
start = time.monotonic()
|
| 802 |
+
async with httpx.AsyncClient(timeout=15, follow_redirects=False) as client:
|
| 803 |
+
response = await client.get(target, headers={"accept": "application/json,text/html;q=0.9"})
|
| 804 |
+
final = urlsplit(str(response.url))
|
| 805 |
+
if final.hostname not in ALLOWED_PROBE_HOSTS:
|
| 806 |
+
raise RuntimeError("probe redirect left the allowlist")
|
| 807 |
+
return {
|
| 808 |
+
"status": "SUCCEEDED" if 200 <= response.status_code < 400 else "FAILED",
|
| 809 |
+
"target": target,
|
| 810 |
+
"http_status": response.status_code,
|
| 811 |
+
"latency_ms": int((time.monotonic() - start) * 1000),
|
| 812 |
+
"bytes": len(response.content),
|
| 813 |
+
"content_type": response.headers.get("content-type", ""),
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
def trust_factor(self) -> dict[str, Any]:
|
| 817 |
+
receipts = self.store.list_receipts(200)
|
| 818 |
+
decisions = [
|
| 819 |
+
item["receipt"]["payload"].get("decision")
|
| 820 |
+
for item in receipts
|
| 821 |
+
if item["kind"] == "passport.evaluate"
|
| 822 |
+
]
|
| 823 |
+
counts = {name: decisions.count(name) for name in ("ALLOW", "BLOCK", "REQUIRE_APPROVAL")}
|
| 824 |
+
total = sum(counts.values())
|
| 825 |
+
penalty = counts["BLOCK"] * 10 + counts["REQUIRE_APPROVAL"] * 3
|
| 826 |
+
score = 100 if total == 0 else max(0, 100 - (penalty * 100 // max(1, total * 10)))
|
| 827 |
+
return {
|
| 828 |
+
"schema": SCHEMA_TRUST,
|
| 829 |
+
"state": "OBSERVED",
|
| 830 |
+
"total_evaluations": total,
|
| 831 |
+
"counts": counts,
|
| 832 |
+
"score_0_to_100": score,
|
| 833 |
+
"basis": "local signed passport decision receipts",
|
| 834 |
+
"not_a_security_certification": True,
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
|
| 838 |
+
async def _bounded_json(request: Request) -> dict[str, Any]:
|
| 839 |
+
content_type = request.headers.get("content-type", "").split(";", 1)[0].lower()
|
| 840 |
+
if content_type != "application/json":
|
| 841 |
+
raise HTTPException(status_code=415, detail="content-type must be application/json")
|
| 842 |
+
declared = request.headers.get("content-length")
|
| 843 |
+
if declared:
|
| 844 |
+
try:
|
| 845 |
+
if int(declared) > MAX_BODY:
|
| 846 |
+
raise HTTPException(status_code=413, detail="request exceeds 64 KiB")
|
| 847 |
+
except ValueError as exc:
|
| 848 |
+
raise HTTPException(status_code=400, detail="invalid content-length") from exc
|
| 849 |
+
body = await request.body()
|
| 850 |
+
if len(body) > MAX_BODY:
|
| 851 |
+
raise HTTPException(status_code=413, detail="request exceeds 64 KiB")
|
| 852 |
+
try:
|
| 853 |
+
value = json.loads(body.decode("utf-8"))
|
| 854 |
+
except Exception as exc:
|
| 855 |
+
raise HTTPException(status_code=400, detail="request must be UTF-8 JSON") from exc
|
| 856 |
+
if not isinstance(value, dict):
|
| 857 |
+
raise HTTPException(status_code=422, detail="request must be one JSON object")
|
| 858 |
+
return value
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
def _asset(name: str) -> str:
|
| 862 |
+
path = Path(__file__).resolve().parent / "series_a_web" / name
|
| 863 |
+
if not path.is_file():
|
| 864 |
+
raise HTTPException(status_code=404, detail=f"asset missing: {name}")
|
| 865 |
+
return path.read_text(encoding="utf-8")
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
def register(app: FastAPI, ns: str = "a11oy", *, db_path: str | None = None) -> dict[str, Any]:
|
| 869 |
+
if any(getattr(route, "path", None) == f"/api/{ns}/v1/series-a/status" for route in app.router.routes):
|
| 870 |
+
return {"ok": True, "state": "ALREADY_REGISTERED", "routes": []}
|
| 871 |
+
|
| 872 |
+
service = Service(db_path)
|
| 873 |
+
prefix = f"/api/{ns}/v1/series-a"
|
| 874 |
+
|
| 875 |
+
async def page(request: Request) -> Response:
|
| 876 |
+
if request.method == "HEAD":
|
| 877 |
+
return Response(status_code=200, media_type="text/html")
|
| 878 |
+
return HTMLResponse(_asset("index.html"), headers={"cache-control": "no-store"})
|
| 879 |
+
|
| 880 |
+
async def js(request: Request) -> Response:
|
| 881 |
+
if request.method == "HEAD":
|
| 882 |
+
return Response(status_code=200, media_type="application/javascript")
|
| 883 |
+
return Response(_asset("app.js"), media_type="application/javascript", headers={"cache-control": "public,max-age=300"})
|
| 884 |
+
|
| 885 |
+
async def css(request: Request) -> Response:
|
| 886 |
+
if request.method == "HEAD":
|
| 887 |
+
return Response(status_code=200, media_type="text/css")
|
| 888 |
+
return Response(_asset("styles.css"), media_type="text/css", headers={"cache-control": "public,max-age=300"})
|
| 889 |
+
|
| 890 |
+
async def status(request: Request) -> Response:
|
| 891 |
+
payload = service.latest_status()
|
| 892 |
+
if request.method == "HEAD":
|
| 893 |
+
return Response(status_code=200, media_type="application/json")
|
| 894 |
+
return JSONResponse(payload, headers={"cache-control": "no-store"})
|
| 895 |
+
|
| 896 |
+
async def manifest(request: Request) -> Response:
|
| 897 |
+
latest = service.store.latest_snapshot()
|
| 898 |
+
if latest is None:
|
| 899 |
+
payload = {"schema": SCHEMA_MANIFEST, "status": "PENDING", "terminal": True}
|
| 900 |
+
else:
|
| 901 |
+
payload = latest
|
| 902 |
+
if request.method == "HEAD":
|
| 903 |
+
return Response(status_code=200, media_type="application/json")
|
| 904 |
+
return JSONResponse(payload, headers={"cache-control": "no-store"})
|
| 905 |
+
|
| 906 |
+
async def refresh(request: Request) -> Response:
|
| 907 |
+
body = await _bounded_json(request)
|
| 908 |
+
actor = str(body.get("actor") or "operator")[:120]
|
| 909 |
+
return JSONResponse(await service.refresh(actor))
|
| 910 |
+
|
| 911 |
+
async def evaluate(request: Request) -> Response:
|
| 912 |
+
return JSONResponse(service.evaluate_passport(await _bounded_json(request)))
|
| 913 |
+
|
| 914 |
+
async def execute(request: Request) -> Response:
|
| 915 |
+
return JSONResponse(await service.execute(await _bounded_json(request)))
|
| 916 |
+
|
| 917 |
+
async def receipts(request: Request) -> Response:
|
| 918 |
+
if request.method == "HEAD":
|
| 919 |
+
return Response(status_code=200, media_type="application/json")
|
| 920 |
+
return JSONResponse({"schema": "szl.series-a-receipts/v1", "items": service.store.list_receipts(50)})
|
| 921 |
+
|
| 922 |
+
async def trust(request: Request) -> Response:
|
| 923 |
+
if request.method == "HEAD":
|
| 924 |
+
return Response(status_code=200, media_type="application/json")
|
| 925 |
+
return JSONResponse(service.trust_factor())
|
| 926 |
+
|
| 927 |
+
async def public_key(request: Request) -> Response:
|
| 928 |
+
if request.method == "HEAD":
|
| 929 |
+
return Response(status_code=200, media_type="text/plain")
|
| 930 |
+
if not service.signer.public_pem:
|
| 931 |
+
return JSONResponse({"state": "UNAVAILABLE", "reason": service.signer.error}, status_code=503)
|
| 932 |
+
return Response(service.signer.public_pem, media_type="text/plain", headers={"cache-control": "public,max-age=300"})
|
| 933 |
+
|
| 934 |
+
async def events(request: Request) -> StreamingResponse:
|
| 935 |
+
last = int(request.query_params.get("after", "0") or 0)
|
| 936 |
+
|
| 937 |
+
async def generate() -> AsyncIterator[bytes]:
|
| 938 |
+
cursor = max(0, last)
|
| 939 |
+
for _ in range(120):
|
| 940 |
+
values = service.store.events_since(cursor)
|
| 941 |
+
for event in values:
|
| 942 |
+
cursor = event["sequence"]
|
| 943 |
+
yield f"id: {cursor}\nevent: {event['kind']}\ndata: {json.dumps(event, separators=(',', ':'))}\n\n".encode()
|
| 944 |
+
if await request.is_disconnected():
|
| 945 |
+
break
|
| 946 |
+
yield b": heartbeat\n\n"
|
| 947 |
+
await asyncio.sleep(1)
|
| 948 |
+
|
| 949 |
+
return StreamingResponse(generate(), media_type="text/event-stream", headers={"cache-control": "no-store"})
|
| 950 |
+
|
| 951 |
+
routes: list[tuple[str, Callable[..., Any], list[str]]] = [
|
| 952 |
+
("/series-a", page, ["GET", "HEAD"]),
|
| 953 |
+
("/series-a/app.js", js, ["GET", "HEAD"]),
|
| 954 |
+
("/series-a/styles.css", css, ["GET", "HEAD"]),
|
| 955 |
+
(f"{prefix}/status", status, ["GET", "HEAD"]),
|
| 956 |
+
(f"{prefix}/manifest", manifest, ["GET", "HEAD"]),
|
| 957 |
+
(f"{prefix}/refresh", refresh, ["POST"]),
|
| 958 |
+
(f"{prefix}/passports/evaluate", evaluate, ["POST"]),
|
| 959 |
+
(f"{prefix}/passports/execute", execute, ["POST"]),
|
| 960 |
+
(f"{prefix}/receipts", receipts, ["GET", "HEAD"]),
|
| 961 |
+
(f"{prefix}/trust", trust, ["GET", "HEAD"]),
|
| 962 |
+
(f"{prefix}/public-key", public_key, ["GET", "HEAD"]),
|
| 963 |
+
(f"{prefix}/events", events, ["GET"]),
|
| 964 |
+
]
|
| 965 |
+
added: list[str] = []
|
| 966 |
+
for path, endpoint, methods in routes:
|
| 967 |
+
app.add_api_route(path, endpoint, methods=methods, include_in_schema=False)
|
| 968 |
+
added.append(path)
|
| 969 |
+
|
| 970 |
+
route_set = set(added)
|
| 971 |
+
selected = [route for route in app.router.routes if getattr(route, "path", None) in route_set]
|
| 972 |
+
selected_ids = {id(route) for route in selected}
|
| 973 |
+
app.router.routes[:] = selected + [route for route in app.router.routes if id(route) not in selected_ids]
|
| 974 |
+
|
| 975 |
+
app.state.szl_series_a_service = service
|
| 976 |
+
add_handler = getattr(app, "add_event_handler", None)
|
| 977 |
+
if callable(add_handler):
|
| 978 |
+
add_handler("startup", service.start)
|
| 979 |
+
|
| 980 |
+
return {
|
| 981 |
+
"ok": True,
|
| 982 |
+
"state": "REGISTERED",
|
| 983 |
+
"namespace": ns,
|
| 984 |
+
"routes": sorted(added),
|
| 985 |
+
"database": service.store.path,
|
| 986 |
+
"signing_key_source": service.signer.source,
|
| 987 |
+
"sign_on_read": False,
|
| 988 |
+
"effectors": sorted(ALLOWED_ACTIONS),
|
| 989 |
+
"max_attempts": 1,
|
| 990 |
+
"private_reasoning_collected": False,
|
| 991 |
+
}
|