Spaces:
Running
Running
feat: add evidence-bound Council v2 and verifier contract
#2
by betterwithage - opened
- README.md +12 -0
- a11oy_ayllu.py +281 -8
- a11oy_nemo_core.py +46 -2
- serve.py +17 -31
- szl_public_verify.py +62 -5
- test/test_council_contract.py +82 -0
- test/test_public_verify_routes.py +56 -0
README.md
CHANGED
|
@@ -17,6 +17,8 @@ tags:
|
|
| 17 |
- a11oy
|
| 18 |
- slsa-l1
|
| 19 |
- apache-2.0
|
|
|
|
|
|
|
| 20 |
ecosystem-stage: "operational"
|
| 21 |
---
|
| 22 |
|
|
@@ -115,6 +117,16 @@ curl -s https://a-11-oy.com/api/a11oy/v1/honest | jq .doctrine_lock.lambda
|
|
| 115 |
| Live energy ledger | [a-11-oy.com/api/a11oy/v1/energy/ledger](https://a-11-oy.com/api/a11oy/v1/energy/ledger) |
|
| 116 |
| Doctrine posture | [a-11-oy.com/api/a11oy/v1/honest](https://a-11-oy.com/api/a11oy/v1/honest) |
|
| 117 |
| WILLAY classifiers | [a-11-oy.com/api/a11oy/v1/willay/classifiers](https://a-11-oy.com/api/a11oy/v1/willay/classifiers) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
### Persistent receipt storage (HF Space)
|
| 120 |
|
|
|
|
| 17 |
- a11oy
|
| 18 |
- slsa-l1
|
| 19 |
- apache-2.0
|
| 20 |
+
models:
|
| 21 |
+
- SZLHOLDINGS/szl-nemo
|
| 22 |
ecosystem-stage: "operational"
|
| 23 |
---
|
| 24 |
|
|
|
|
| 117 |
| Live energy ledger | [a-11-oy.com/api/a11oy/v1/energy/ledger](https://a-11-oy.com/api/a11oy/v1/energy/ledger) |
|
| 118 |
| Doctrine posture | [a-11-oy.com/api/a11oy/v1/honest](https://a-11-oy.com/api/a11oy/v1/honest) |
|
| 119 |
| WILLAY classifiers | [a-11-oy.com/api/a11oy/v1/willay/classifiers](https://a-11-oy.com/api/a11oy/v1/willay/classifiers) |
|
| 120 |
+
| Evidence-bound Council | [a-11-oy.com/api/a11oy/v1/ayllu/council/manifest](https://a-11-oy.com/api/a11oy/v1/ayllu/council/manifest) |
|
| 121 |
+
| SZL-Nemo card | [a-11-oy.com/api/a11oy/v1/nemo/card](https://a-11-oy.com/api/a11oy/v1/nemo/card) |
|
| 122 |
+
| Receipt verifier contract | [a-11-oy.com/api/a11oy/v1/verify/receipt](https://a-11-oy.com/api/a11oy/v1/verify/receipt) |
|
| 123 |
+
|
| 124 |
+
Council v2 is proposal-only. It adds a signed Nemo routing trace, per-turn output
|
| 125 |
+
digests, explicit limits, a deterministic replay key, and a required human
|
| 126 |
+
checkpoint. Semantic consensus and Council effectiveness are labeled
|
| 127 |
+
`NOT_MEASURED` until golden-case evaluation exists. The linked `szl-nemo` Hub
|
| 128 |
+
artifact is currently a model card and Modelfile recipe; it contains no SZL-trained
|
| 129 |
+
weights and is not described as a fine-tune.
|
| 130 |
|
| 131 |
### Persistent receipt storage (HF Space)
|
| 132 |
|
a11oy_ayllu.py
CHANGED
|
@@ -71,6 +71,10 @@ COUNCIL_MAX = 5 # hard cap on participants /
|
|
| 71 |
COUNCIL_DEBATE_MAX = 3 # debate doubles model calls; tighter cap bounds cost
|
| 72 |
COUNCIL_DEFAULT = ["Amaru", "Kamachiq", "Qhatuq"] # architect · orchestrator · markets
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# One process-wide lounge (in-memory, honest source labels).
|
| 75 |
_LOUNGE = Lounge()
|
| 76 |
|
|
@@ -109,13 +113,26 @@ def _receipt_sha(receipt: Optional[dict]) -> Optional[str]:
|
|
| 109 |
return None
|
| 110 |
|
| 111 |
|
| 112 |
-
def _make_receipt(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 113 |
"""Wrap payload in a DSSE envelope (honest UNSIGNED if no cosign key)."""
|
| 114 |
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
| 115 |
honesty = "UNSIGNED — szl_dsse not present; no signature fabricated."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
if _dsse is not None:
|
| 117 |
try:
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
except Exception as exc:
|
| 120 |
honesty = (f"UNSIGNED — szl_dsse.sign raised ({str(exc)[:80]}); "
|
| 121 |
"no signature fabricated.")
|
|
@@ -128,6 +145,213 @@ def _make_receipt(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 128 |
}
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
# --- The /ayllu page: a working chat + council UI. 0-CDN (pure inline markup, no
|
| 132 |
# external assets). `__NS__`/`__VERSION__` are substituted at render time so the
|
| 133 |
# JS+CSS braces stay literal (no f-string brace-doubling). --------------------
|
|
@@ -172,6 +396,9 @@ font-size:11px;font-weight:800;color:var(--fg);border:1px solid var(--line);flex
|
|
| 172 |
.arch{color:var(--dim);font-size:12px}
|
| 173 |
.ans{margin-top:7px;white-space:pre-wrap}
|
| 174 |
.rcpt{color:var(--dim);font-size:12px;margin-top:8px}
|
|
|
|
|
|
|
|
|
|
| 175 |
.note{color:#da3;font-size:12px;margin-bottom:8px}
|
| 176 |
.err{color:#e66}
|
| 177 |
.stub{color:#da3;font-weight:700;font-size:11px}
|
|
@@ -230,6 +457,11 @@ section[id]{scroll-margin-top:72px}
|
|
| 230 |
|
| 231 |
<section class="card" id="sec-council">
|
| 232 |
<h2>Convene a council</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
<p class="hint">Defaults to 3 core personas; select up to 5 (⌘/Ctrl-click). Fan-out is
|
| 234 |
capped to protect cost. Debate mode runs exactly two bounded rounds
|
| 235 |
(after arXiv:2305.14325) and is capped to 3 personas.</p>
|
|
@@ -370,10 +602,19 @@ document.getElementById('councilbtn').onclick=async()=>{
|
|
| 370 |
headers:{'content-type':'application/json'},body:JSON.stringify(body)});
|
| 371 |
if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
|
| 372 |
+(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
|
| 373 |
-
const res=data.result||{}, rounds=res.rounds||[];
|
| 374 |
const cap=res.cap_note?('<div class="note">'+esc(res.cap_note)+'</div>'):'';
|
| 375 |
const r1=rounds.filter(t=>(t.round||1)===1), r2=rounds.filter(t=>t.round===2);
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
if(r2.length){
|
| 378 |
html+='<div class="roundhdr">Round 1 — opening positions</div>'+r1.map(renderTurn).join('');
|
| 379 |
html+='<div class="roundhdr">Round 2 — debate & converge (final)</div>'+r2.map(renderTurn).join('');
|
|
@@ -465,6 +706,14 @@ def register(app, ns: str = "a11oy") -> str:
|
|
| 465 |
from fastapi import Request
|
| 466 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
async def _roster(request: "Request") -> "JSONResponse":
|
| 469 |
return JSONResponse({
|
| 470 |
"count": len(ROSTER),
|
|
@@ -477,6 +726,9 @@ def register(app, ns: str = "a11oy") -> str:
|
|
| 477 |
"version": __version__,
|
| 478 |
})
|
| 479 |
|
|
|
|
|
|
|
|
|
|
| 480 |
async def _ask(request: "Request") -> "JSONResponse":
|
| 481 |
ok, retry = _ASK_BUCKET.check()
|
| 482 |
if not ok:
|
|
@@ -523,7 +775,7 @@ def register(app, ns: str = "a11oy") -> str:
|
|
| 523 |
"stub": turn.get("stub"),
|
| 524 |
"energy_receipt_sha256": _receipt_sha(turn.get("energy_receipt")),
|
| 525 |
"honesty": turn.get("honesty"),
|
| 526 |
-
})
|
| 527 |
_LOUNGE.post(
|
| 528 |
p.name, turn.get("answer") or turn.get("honesty"),
|
| 529 |
source=("brain" if turn.get("answer") is not None else "persona-fallback"))
|
|
@@ -579,15 +831,33 @@ def register(app, ns: str = "a11oy") -> str:
|
|
| 579 |
if cap_note:
|
| 580 |
result["cap_note"] = cap_note
|
| 581 |
council_id = str(uuid.uuid4())
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
"council_id": council_id,
|
| 584 |
"prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
|
| 585 |
"participants": result["participants"],
|
| 586 |
"mode": result.get("mode"),
|
| 587 |
"models": [r.get("model") for r in result.get("rounds", [])],
|
| 588 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
return JSONResponse({"council_id": council_id, "result": result,
|
| 590 |
-
"receipt": receipt})
|
| 591 |
|
| 592 |
async def _lounge_feed(request: "Request") -> "JSONResponse":
|
| 593 |
return JSONResponse({"count": len(_LOUNGE.feed),
|
|
@@ -602,6 +872,9 @@ def register(app, ns: str = "a11oy") -> str:
|
|
| 602 |
app.add_api_route(f"/api/{ns}/v1/ayllu/ask", _ask, methods=["POST"],
|
| 603 |
tags=["ayllu"],
|
| 604 |
summary="Ask one persona — bounded, honest, receipted")
|
|
|
|
|
|
|
|
|
|
| 605 |
app.add_api_route(f"/api/{ns}/v1/ayllu/council", _council, methods=["POST"],
|
| 606 |
tags=["ayllu"],
|
| 607 |
summary="Bounded multi-persona deliberation (capped fan-out; optional 2-round debate mode after arXiv:2305.14325)")
|
|
|
|
| 71 |
COUNCIL_DEBATE_MAX = 3 # debate doubles model calls; tighter cap bounds cost
|
| 72 |
COUNCIL_DEFAULT = ["Amaru", "Kamachiq", "Qhatuq"] # architect · orchestrator · markets
|
| 73 |
|
| 74 |
+
COUNCIL_CONTRACT_VERSION = "2.0"
|
| 75 |
+
COUNCIL_SCHEMA = "szl.ayllu.evidence-bound-council/v2"
|
| 76 |
+
NEMO_ARTIFACT = "https://huggingface.co/SZLHOLDINGS/szl-nemo"
|
| 77 |
+
|
| 78 |
# One process-wide lounge (in-memory, honest source labels).
|
| 79 |
_LOUNGE = Lounge()
|
| 80 |
|
|
|
|
| 113 |
return None
|
| 114 |
|
| 115 |
|
| 116 |
+
def _make_receipt(payload: Dict[str, Any], sign_fn=None) -> Dict[str, Any]:
|
| 117 |
"""Wrap payload in a DSSE envelope (honest UNSIGNED if no cosign key)."""
|
| 118 |
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
| 119 |
honesty = "UNSIGNED — szl_dsse not present; no signature fabricated."
|
| 120 |
+
if callable(sign_fn):
|
| 121 |
+
try:
|
| 122 |
+
env = sign_fn(payload)
|
| 123 |
+
if isinstance(env, dict):
|
| 124 |
+
return env
|
| 125 |
+
honesty = "UNSIGNED - runtime signer returned a non-object; no signature fabricated."
|
| 126 |
+
except Exception as exc:
|
| 127 |
+
honesty = (f"UNSIGNED - runtime signer raised ({str(exc)[:80]}); "
|
| 128 |
+
"no signature fabricated.")
|
| 129 |
if _dsse is not None:
|
| 130 |
try:
|
| 131 |
+
# szl_dsse exposes sign_payload(); the former sign() call made every
|
| 132 |
+
# Ayllu receipt fall through to UNSIGNED even when the module existed.
|
| 133 |
+
if hasattr(_dsse, "sign_payload"):
|
| 134 |
+
return _dsse.sign_payload(
|
| 135 |
+
payload, "application/vnd.szl.receipt+json")
|
| 136 |
except Exception as exc:
|
| 137 |
honesty = (f"UNSIGNED — szl_dsse.sign raised ({str(exc)[:80]}); "
|
| 138 |
"no signature fabricated.")
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
|
| 148 |
+
def _sha256_json(value: Any) -> str:
|
| 149 |
+
body = json.dumps(value, sort_keys=True, separators=(",", ":"),
|
| 150 |
+
ensure_ascii=False).encode("utf-8")
|
| 151 |
+
return hashlib.sha256(body).hexdigest()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def council_manifest(ns: str = "a11oy") -> Dict[str, Any]:
|
| 155 |
+
"""Side-effect-free, investor-readable contract for the bounded council."""
|
| 156 |
+
base = f"/api/{ns}/v1/ayllu"
|
| 157 |
+
return {
|
| 158 |
+
"schema": "szl.ayllu.council-manifest/v1",
|
| 159 |
+
"contract_schema": COUNCIL_SCHEMA,
|
| 160 |
+
"contract_version": COUNCIL_CONTRACT_VERSION,
|
| 161 |
+
"purpose": (
|
| 162 |
+
"Produce a bounded, evidence-bearing advisory record from multiple "
|
| 163 |
+
"personas. The Council proposes; it never executes an external action."
|
| 164 |
+
),
|
| 165 |
+
"try": {
|
| 166 |
+
"method": "POST",
|
| 167 |
+
"endpoint": base + "/council",
|
| 168 |
+
"body": {"prompt": "Review this proposed agent action",
|
| 169 |
+
"personas": ["Amaru", "Yupaq", "Kamachiq"],
|
| 170 |
+
"debate": True},
|
| 171 |
+
},
|
| 172 |
+
"evidence": [
|
| 173 |
+
"prompt SHA-256",
|
| 174 |
+
"persona/model/round/output digest per turn",
|
| 175 |
+
"Nemo governed-route decision and DSSE receipt",
|
| 176 |
+
"deterministic replay key over participants, mode, and output digests",
|
| 177 |
+
"in-process Khipu chain receipt (resets on process restart)",
|
| 178 |
+
"outer Council DSSE receipt",
|
| 179 |
+
],
|
| 180 |
+
"limits": {
|
| 181 |
+
"prompt_chars": MAX_PROMPT_CHARS,
|
| 182 |
+
"participants": COUNCIL_MAX,
|
| 183 |
+
"debate_participants": COUNCIL_DEBATE_MAX,
|
| 184 |
+
"debate_rounds": 2,
|
| 185 |
+
"effectors": "none",
|
| 186 |
+
"decision_state": "PROPOSAL_ONLY",
|
| 187 |
+
"semantic_consensus": "NOT_MEASURED",
|
| 188 |
+
"chain_persistence": "IN_MEMORY_RESETS_ON_RESTART",
|
| 189 |
+
},
|
| 190 |
+
"reproduce": {
|
| 191 |
+
"manifest": base + "/council/manifest",
|
| 192 |
+
"verifier": f"/api/{ns}/v1/verify/receipt",
|
| 193 |
+
"public_key": "/cosign.pub",
|
| 194 |
+
},
|
| 195 |
+
"nemo": {
|
| 196 |
+
"artifact": NEMO_ARTIFACT,
|
| 197 |
+
"artifact_kind": "configuration-recipe",
|
| 198 |
+
"weights_present": False,
|
| 199 |
+
"training_state": "NOT_PERFORMED",
|
| 200 |
+
"honesty": (
|
| 201 |
+
"The current SZL-Nemo Hub artifact is a card and Modelfile recipe, "
|
| 202 |
+
"not SZL-trained weights. Council answers use the live A11OY router."
|
| 203 |
+
),
|
| 204 |
+
},
|
| 205 |
+
"evaluation": {
|
| 206 |
+
"council_effectiveness": "NOT_MEASURED",
|
| 207 |
+
"required_next": (
|
| 208 |
+
"golden cases with decision-quality, calibration, dissent-recall, "
|
| 209 |
+
"cost, latency, and human-overturn outcomes"
|
| 210 |
+
),
|
| 211 |
+
},
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _nemo_council_route(prompt: str, sign_fn=None) -> Dict[str, Any]:
|
| 216 |
+
"""Run the existing Nemo router and expose the evidence Council needs."""
|
| 217 |
+
try:
|
| 218 |
+
import a11oy_nemo_core as nemo
|
| 219 |
+
routed = nemo.govern_route(prompt, top_k=3, sign_fn=sign_fn)
|
| 220 |
+
experts = routed.get("experts") or []
|
| 221 |
+
return {
|
| 222 |
+
"state": routed.get("routing_evidence_state", "HEURISTIC"),
|
| 223 |
+
"source": "a11oy_nemo_core.govern_route",
|
| 224 |
+
"model": routed.get("model"),
|
| 225 |
+
"model_version": routed.get("model_version"),
|
| 226 |
+
"experts_selected": routed.get("experts_selected") or [],
|
| 227 |
+
"selection_basis": [e.get("selection_basis") for e in experts],
|
| 228 |
+
"overall_lambda_advisory": routed.get("overall_lambda_advisory"),
|
| 229 |
+
"below_advisory_floor": any(bool(e.get("below_advisory_floor"))
|
| 230 |
+
for e in experts),
|
| 231 |
+
"limits": routed.get("routing_limits"),
|
| 232 |
+
"receipt": routed.get("receipt"),
|
| 233 |
+
}
|
| 234 |
+
except Exception as exc:
|
| 235 |
+
return {
|
| 236 |
+
"state": "UNAVAILABLE",
|
| 237 |
+
"source": "a11oy_nemo_core.govern_route",
|
| 238 |
+
"error": type(exc).__name__,
|
| 239 |
+
"honesty": "Nemo routing unavailable; no route or score fabricated.",
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _build_council_contract(prompt: str, result: Dict[str, Any],
|
| 244 |
+
nemo_route: Dict[str, Any]) -> Dict[str, Any]:
|
| 245 |
+
rounds = result.get("rounds") or []
|
| 246 |
+
turn_evidence = []
|
| 247 |
+
for turn in rounds:
|
| 248 |
+
answer = turn.get("answer")
|
| 249 |
+
turn_evidence.append({
|
| 250 |
+
"persona": turn.get("persona"),
|
| 251 |
+
"round": turn.get("round"),
|
| 252 |
+
"model": turn.get("model"),
|
| 253 |
+
"stub": bool(turn.get("stub")),
|
| 254 |
+
"output_sha256": (hashlib.sha256(str(answer).encode("utf-8")).hexdigest()
|
| 255 |
+
if answer is not None else None),
|
| 256 |
+
"energy_receipt_sha256": _receipt_sha(turn.get("energy_receipt")),
|
| 257 |
+
})
|
| 258 |
+
replay_material = {
|
| 259 |
+
"contract_version": COUNCIL_CONTRACT_VERSION,
|
| 260 |
+
"prompt_sha256": hashlib.sha256(prompt.encode("utf-8")).hexdigest(),
|
| 261 |
+
"participants": result.get("participants") or [],
|
| 262 |
+
"mode": result.get("mode"),
|
| 263 |
+
"turns": turn_evidence,
|
| 264 |
+
}
|
| 265 |
+
live_turns = sum(1 for t in turn_evidence if not t["stub"])
|
| 266 |
+
if not turn_evidence:
|
| 267 |
+
evidence_state = "UNAVAILABLE"
|
| 268 |
+
elif live_turns == len(turn_evidence):
|
| 269 |
+
evidence_state = "LIVE"
|
| 270 |
+
elif live_turns:
|
| 271 |
+
evidence_state = "MIXED"
|
| 272 |
+
else:
|
| 273 |
+
evidence_state = "STUB"
|
| 274 |
+
return {
|
| 275 |
+
"schema": COUNCIL_SCHEMA,
|
| 276 |
+
"contract_version": COUNCIL_CONTRACT_VERSION,
|
| 277 |
+
"purpose": "bounded multi-persona advisory deliberation",
|
| 278 |
+
"decision_state": "PROPOSAL_ONLY",
|
| 279 |
+
"approval_state": "HUMAN_REVIEW_REQUIRED",
|
| 280 |
+
"evidence_state": evidence_state,
|
| 281 |
+
"prompt_sha256": replay_material["prompt_sha256"],
|
| 282 |
+
"turn_evidence": turn_evidence,
|
| 283 |
+
"routing": nemo_route,
|
| 284 |
+
"formula_path": [
|
| 285 |
+
{"id": "lambda-aggregate", "state": "CONJECTURE_1_ADVISORY"},
|
| 286 |
+
{"id": "active-flux-crossover", "state": "MODELED"},
|
| 287 |
+
{"id": "dsse-pae", "state": "IMPLEMENTED"},
|
| 288 |
+
],
|
| 289 |
+
"semantic_consensus": {
|
| 290 |
+
"state": "NOT_MEASURED",
|
| 291 |
+
"honesty": (
|
| 292 |
+
"Multiple answers do not prove consensus or correctness. Semantic "
|
| 293 |
+
"agreement, dissent recall, and decision quality require labeled evals."
|
| 294 |
+
),
|
| 295 |
+
},
|
| 296 |
+
"limits": {
|
| 297 |
+
"participants_max": COUNCIL_MAX,
|
| 298 |
+
"debate_participants_max": COUNCIL_DEBATE_MAX,
|
| 299 |
+
"rounds_max": 2,
|
| 300 |
+
"model_calls_observed": len(turn_evidence),
|
| 301 |
+
"external_effectors": 0,
|
| 302 |
+
"automatic_commit": False,
|
| 303 |
+
},
|
| 304 |
+
"human_checkpoint": {
|
| 305 |
+
"required": True,
|
| 306 |
+
"satisfied": False,
|
| 307 |
+
"next_action": "Review evidence and explicitly approve, revise, or reject.",
|
| 308 |
+
},
|
| 309 |
+
"replay": {
|
| 310 |
+
"key": "sha256:" + _sha256_json(replay_material),
|
| 311 |
+
"material": replay_material,
|
| 312 |
+
"verifier": "/api/a11oy/v1/verify/receipt",
|
| 313 |
+
"public_key": "/cosign.pub",
|
| 314 |
+
},
|
| 315 |
+
"training": {
|
| 316 |
+
"artifact": NEMO_ARTIFACT,
|
| 317 |
+
"weights_present": False,
|
| 318 |
+
"state": "NOT_PERFORMED",
|
| 319 |
+
},
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def _mint_council_chain(contract: Dict[str, Any], ns: str = "a11oy") -> Dict[str, Any]:
|
| 324 |
+
"""Append the proposal receipt to the shared in-process Khipu chain."""
|
| 325 |
+
try:
|
| 326 |
+
import szl_khipu
|
| 327 |
+
dag = szl_khipu.get_dag("ayllu_council", ns=ns)
|
| 328 |
+
receipt = dag.emit("ayllu.council.proposal", {
|
| 329 |
+
"contract_version": contract.get("contract_version"),
|
| 330 |
+
"decision_state": contract.get("decision_state"),
|
| 331 |
+
"evidence_state": contract.get("evidence_state"),
|
| 332 |
+
"prompt_sha256": contract.get("prompt_sha256"),
|
| 333 |
+
"replay_key": (contract.get("replay") or {}).get("key"),
|
| 334 |
+
})
|
| 335 |
+
chain = dag.verify_chain()
|
| 336 |
+
return {
|
| 337 |
+
"state": "LIVE",
|
| 338 |
+
"organ": "ayllu_council",
|
| 339 |
+
"receipt_id": receipt.get("digest"),
|
| 340 |
+
"seq": receipt.get("seq"),
|
| 341 |
+
"chain_verified": bool(chain.get("ok")),
|
| 342 |
+
"depth": dag.depth(),
|
| 343 |
+
"persistence": "IN_MEMORY_RESETS_ON_RESTART",
|
| 344 |
+
}
|
| 345 |
+
except Exception as exc:
|
| 346 |
+
return {
|
| 347 |
+
"state": "UNAVAILABLE",
|
| 348 |
+
"organ": "ayllu_council",
|
| 349 |
+
"receipt_id": None,
|
| 350 |
+
"error": type(exc).__name__,
|
| 351 |
+
"honesty": "Khipu append unavailable; no chain receipt fabricated.",
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
|
| 355 |
# --- The /ayllu page: a working chat + council UI. 0-CDN (pure inline markup, no
|
| 356 |
# external assets). `__NS__`/`__VERSION__` are substituted at render time so the
|
| 357 |
# JS+CSS braces stay literal (no f-string brace-doubling). --------------------
|
|
|
|
| 396 |
.arch{color:var(--dim);font-size:12px}
|
| 397 |
.ans{margin-top:7px;white-space:pre-wrap}
|
| 398 |
.rcpt{color:var(--dim);font-size:12px;margin-top:8px}
|
| 399 |
+
.contract{border-left:3px solid var(--teal);padding:9px 11px;margin:10px 0;
|
| 400 |
+
background:#071711;color:var(--dim);font-size:12px;line-height:1.6}
|
| 401 |
+
.contract b{color:var(--fg)}
|
| 402 |
.note{color:#da3;font-size:12px;margin-bottom:8px}
|
| 403 |
.err{color:#e66}
|
| 404 |
.stub{color:#da3;font-weight:700;font-size:11px}
|
|
|
|
| 457 |
|
| 458 |
<section class="card" id="sec-council">
|
| 459 |
<h2>Convene a council</h2>
|
| 460 |
+
<div class="contract"><b>Evidence-bound Council v2.</b> Every run carries a
|
| 461 |
+
Nemo route receipt, per-turn output digests, a replay key, explicit limits, and a
|
| 462 |
+
human checkpoint. State is always <code>PROPOSAL_ONLY</code>; semantic consensus and
|
| 463 |
+
Council effectiveness remain <code>NOT_MEASURED</code> until labeled evaluation exists.
|
| 464 |
+
<a href="/api/__NS__/v1/ayllu/council/manifest">machine-readable contract</a></div>
|
| 465 |
<p class="hint">Defaults to 3 core personas; select up to 5 (⌘/Ctrl-click). Fan-out is
|
| 466 |
capped to protect cost. Debate mode runs exactly two bounded rounds
|
| 467 |
(after arXiv:2305.14325) and is capped to 3 personas.</p>
|
|
|
|
| 602 |
headers:{'content-type':'application/json'},body:JSON.stringify(body)});
|
| 603 |
if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
|
| 604 |
+(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
|
| 605 |
+
const res=data.result||{}, rounds=res.rounds||[], c=data.contract||{};
|
| 606 |
const cap=res.cap_note?('<div class="note">'+esc(res.cap_note)+'</div>'):'';
|
| 607 |
const r1=rounds.filter(t=>(t.round||1)===1), r2=rounds.filter(t=>t.round===2);
|
| 608 |
+
const route=c.routing||{}, outer=data.receipt||{};
|
| 609 |
+
const contract=`<div class="contract"><b>${esc(c.decision_state||'PROPOSAL_ONLY')}</b>`
|
| 610 |
+
+` · evidence ${esc(c.evidence_state||'UNKNOWN')}`
|
| 611 |
+
+` · human review ${c.human_checkpoint&&c.human_checkpoint.required?'REQUIRED':'UNKNOWN'}`
|
| 612 |
+
+`<br>Nemo: ${esc((route.experts_selected||[]).join(' + ')||route.state||'unavailable')}`
|
| 613 |
+
+` · route ${esc(route.state||'UNKNOWN')} · outer receipt ${outer.signed?'SIGNED':'UNSIGNED'}`
|
| 614 |
+
+`<br>replay ${esc((c.replay&&c.replay.key)||'unavailable')}`
|
| 615 |
+
+`<br>semantic consensus: ${esc((c.semantic_consensus&&c.semantic_consensus.state)||'NOT_MEASURED')}`
|
| 616 |
+
+`</div>`;
|
| 617 |
+
let html=cap+contract;
|
| 618 |
if(r2.length){
|
| 619 |
html+='<div class="roundhdr">Round 1 — opening positions</div>'+r1.map(renderTurn).join('');
|
| 620 |
html+='<div class="roundhdr">Round 2 — debate & converge (final)</div>'+r2.map(renderTurn).join('');
|
|
|
|
| 706 |
from fastapi import Request
|
| 707 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 708 |
|
| 709 |
+
def _runtime_signer(request: "Request"):
|
| 710 |
+
"""Resolve the host signer lazily: Ayllu registers before serve.py creates it."""
|
| 711 |
+
try:
|
| 712 |
+
signer = getattr(request.app.state, "szl_sign_receipt", None)
|
| 713 |
+
return signer if callable(signer) else None
|
| 714 |
+
except Exception:
|
| 715 |
+
return None
|
| 716 |
+
|
| 717 |
async def _roster(request: "Request") -> "JSONResponse":
|
| 718 |
return JSONResponse({
|
| 719 |
"count": len(ROSTER),
|
|
|
|
| 726 |
"version": __version__,
|
| 727 |
})
|
| 728 |
|
| 729 |
+
async def _council_manifest(request: "Request") -> "JSONResponse":
|
| 730 |
+
return JSONResponse(council_manifest(ns))
|
| 731 |
+
|
| 732 |
async def _ask(request: "Request") -> "JSONResponse":
|
| 733 |
ok, retry = _ASK_BUCKET.check()
|
| 734 |
if not ok:
|
|
|
|
| 775 |
"stub": turn.get("stub"),
|
| 776 |
"energy_receipt_sha256": _receipt_sha(turn.get("energy_receipt")),
|
| 777 |
"honesty": turn.get("honesty"),
|
| 778 |
+
}, sign_fn=_runtime_signer(request))
|
| 779 |
_LOUNGE.post(
|
| 780 |
p.name, turn.get("answer") or turn.get("honesty"),
|
| 781 |
source=("brain" if turn.get("answer") is not None else "persona-fallback"))
|
|
|
|
| 831 |
if cap_note:
|
| 832 |
result["cap_note"] = cap_note
|
| 833 |
council_id = str(uuid.uuid4())
|
| 834 |
+
signer = _runtime_signer(request)
|
| 835 |
+
nemo_route = _nemo_council_route(prompt, sign_fn=signer)
|
| 836 |
+
contract = _build_council_contract(prompt, result, nemo_route)
|
| 837 |
+
contract["chain"] = _mint_council_chain(contract, ns=ns)
|
| 838 |
+
receipt_body = {
|
| 839 |
"council_id": council_id,
|
| 840 |
"prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
|
| 841 |
"participants": result["participants"],
|
| 842 |
"mode": result.get("mode"),
|
| 843 |
"models": [r.get("model") for r in result.get("rounds", [])],
|
| 844 |
+
"decision_state": contract["decision_state"],
|
| 845 |
+
"evidence_state": contract["evidence_state"],
|
| 846 |
+
"replay_key": contract["replay"]["key"],
|
| 847 |
+
"turn_evidence": contract["turn_evidence"],
|
| 848 |
+
"nemo_route_receipt_sha256": _receipt_sha(
|
| 849 |
+
(contract.get("routing") or {}).get("receipt")),
|
| 850 |
+
"human_checkpoint": contract["human_checkpoint"],
|
| 851 |
+
"chain": contract["chain"],
|
| 852 |
+
}
|
| 853 |
+
receipt = _make_receipt({
|
| 854 |
+
"schema": "szl.ayllu.council-receipt/v2",
|
| 855 |
+
"body": receipt_body,
|
| 856 |
+
"payload_digest": _sha256_json(receipt_body),
|
| 857 |
+
"receipt_id": contract["chain"].get("receipt_id"),
|
| 858 |
+
}, sign_fn=signer)
|
| 859 |
return JSONResponse({"council_id": council_id, "result": result,
|
| 860 |
+
"contract": contract, "receipt": receipt})
|
| 861 |
|
| 862 |
async def _lounge_feed(request: "Request") -> "JSONResponse":
|
| 863 |
return JSONResponse({"count": len(_LOUNGE.feed),
|
|
|
|
| 872 |
app.add_api_route(f"/api/{ns}/v1/ayllu/ask", _ask, methods=["POST"],
|
| 873 |
tags=["ayllu"],
|
| 874 |
summary="Ask one persona — bounded, honest, receipted")
|
| 875 |
+
app.add_api_route(f"/api/{ns}/v1/ayllu/council/manifest", _council_manifest,
|
| 876 |
+
methods=["GET"], tags=["ayllu"],
|
| 877 |
+
summary="Evidence-bound Council contract, limits, and reproduce path")
|
| 878 |
app.add_api_route(f"/api/{ns}/v1/ayllu/council", _council, methods=["POST"],
|
| 879 |
tags=["ayllu"],
|
| 880 |
summary="Bounded multi-persona deliberation (capped fan-out; optional 2-round debate mode after arXiv:2305.14325)")
|
a11oy_nemo_core.py
CHANGED
|
@@ -70,6 +70,17 @@ from datetime import datetime, timezone
|
|
| 70 |
# ---------------------------------------------------------------------------
|
| 71 |
NEMO_NAME = "SZL-Nemo"
|
| 72 |
NEMO_VERSION = "0.1.0-skeleton"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
# The honest base options (open weights, cited). We pick Qwen3-32B (Apache-2.0) as
|
| 75 |
# the DEFAULT sovereign-local base because it fits the 2-GPU plan (TP=2). GLM (MIT)
|
|
@@ -360,11 +371,34 @@ def _select_experts(query: str, top_k: int = 2):
|
|
| 360 |
and select the top_k as the active experts. Deterministic, transparent."""
|
| 361 |
q = (query or "").lower()
|
| 362 |
scored = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
for e in NEMO_EXPERTS:
|
| 364 |
hits = [k for k in e["keywords"] if k in q]
|
| 365 |
score = len(hits) + (0.01 * len(q)) # tiny length tiebreak; deterministic
|
| 366 |
scored.append({"expert": e, "score": score, "matched": hits})
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
chosen = scored[:max(1, top_k)]
|
| 369 |
return scored, chosen
|
| 370 |
|
|
@@ -455,6 +489,7 @@ def govern_route(query: str, top_k: int = 2, pi_bandwidth_hz: float = 12.0,
|
|
| 455 |
experts_out.append({
|
| 456 |
"expert_id": e["id"], "title": e["title"], "desc": e["desc"],
|
| 457 |
"matched_keywords": c["matched"], "selection_score": round(c["score"], 4),
|
|
|
|
| 458 |
"lambda_advisory": lam, "lambda_axes": axis,
|
| 459 |
"lambda_status": DOCTRINE["lambda"],
|
| 460 |
"below_advisory_floor": below_floor,
|
|
@@ -478,7 +513,15 @@ def govern_route(query: str, top_k: int = 2, pi_bandwidth_hz: float = 12.0,
|
|
| 478 |
"experts": experts_out,
|
| 479 |
"all_expert_scores": [{"expert_id": s["expert"]["id"],
|
| 480 |
"score": round(s["score"], 4),
|
| 481 |
-
"matched": s["matched"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
"overall_lambda_advisory": overall_lambda,
|
| 483 |
"thompson_posteriors": _thompson_view(),
|
| 484 |
"doctrine": DOCTRINE,
|
|
@@ -898,6 +941,7 @@ def model_card():
|
|
| 898 |
"one_liner": ("SZL-Nemo — a sovereign, governed, self-improving AGENT model "
|
| 899 |
"built ON an open base (default Qwen3-32B, Apache-2.0)."),
|
| 900 |
"base": NEMO_BASE,
|
|
|
|
| 901 |
"what_is_ours": [
|
| 902 |
"Governed-MoE DOMAIN-EXPERT router (Λ-governed, signed every selection) — the differentiator.",
|
| 903 |
"MTP / speculative decoding as the inference default (app-layer; box ROADMAP→Forge).",
|
|
|
|
| 70 |
# ---------------------------------------------------------------------------
|
| 71 |
NEMO_NAME = "SZL-Nemo"
|
| 72 |
NEMO_VERSION = "0.1.0-skeleton"
|
| 73 |
+
NEMO_ARTIFACT = {
|
| 74 |
+
"repo_id": "SZLHOLDINGS/szl-nemo",
|
| 75 |
+
"url": "https://huggingface.co/SZLHOLDINGS/szl-nemo",
|
| 76 |
+
"kind": "configuration-recipe",
|
| 77 |
+
"weights_present": False,
|
| 78 |
+
"training_state": "NOT_PERFORMED",
|
| 79 |
+
"honesty": (
|
| 80 |
+
"The Hub artifact currently contains a model card and Modelfile recipe, "
|
| 81 |
+
"not SZL-trained weights. It must not be described as a fine-tuned model."
|
| 82 |
+
),
|
| 83 |
+
}
|
| 84 |
|
| 85 |
# The honest base options (open weights, cited). We pick Qwen3-32B (Apache-2.0) as
|
| 86 |
# the DEFAULT sovereign-local base because it fits the 2-GPU plan (TP=2). GLM (MIT)
|
|
|
|
| 371 |
and select the top_k as the active experts. Deterministic, transparent."""
|
| 372 |
q = (query or "").lower()
|
| 373 |
scored = []
|
| 374 |
+
# When a prompt carries no domain keyword, the old alphabetical tie-break
|
| 375 |
+
# silently selected code + counter-UAS. That was deterministic but not a
|
| 376 |
+
# defensible default for a governed council. The fallback is now explicit:
|
| 377 |
+
# governance first (policy/evidence review), then code (implementation
|
| 378 |
+
# feasibility). It remains a transparent heuristic, never a learned claim.
|
| 379 |
+
fallback_priority = {
|
| 380 |
+
"governance": 0,
|
| 381 |
+
"code": 1,
|
| 382 |
+
"finance": 2,
|
| 383 |
+
"maritime": 3,
|
| 384 |
+
"counter-uas": 4,
|
| 385 |
+
}
|
| 386 |
for e in NEMO_EXPERTS:
|
| 387 |
hits = [k for k in e["keywords"] if k in q]
|
| 388 |
score = len(hits) + (0.01 * len(q)) # tiny length tiebreak; deterministic
|
| 389 |
scored.append({"expert": e, "score": score, "matched": hits})
|
| 390 |
+
has_domain_signal = any(r["matched"] for r in scored)
|
| 391 |
+
if has_domain_signal:
|
| 392 |
+
scored.sort(key=lambda r: (-r["score"],
|
| 393 |
+
fallback_priority.get(r["expert"]["id"], 99),
|
| 394 |
+
r["expert"]["id"]))
|
| 395 |
+
basis = "matched-domain-keywords"
|
| 396 |
+
else:
|
| 397 |
+
scored.sort(key=lambda r: (fallback_priority.get(r["expert"]["id"], 99),
|
| 398 |
+
r["expert"]["id"]))
|
| 399 |
+
basis = "explicit-governance-first-fallback"
|
| 400 |
+
for row in scored:
|
| 401 |
+
row["selection_basis"] = basis
|
| 402 |
chosen = scored[:max(1, top_k)]
|
| 403 |
return scored, chosen
|
| 404 |
|
|
|
|
| 489 |
experts_out.append({
|
| 490 |
"expert_id": e["id"], "title": e["title"], "desc": e["desc"],
|
| 491 |
"matched_keywords": c["matched"], "selection_score": round(c["score"], 4),
|
| 492 |
+
"selection_basis": c.get("selection_basis"),
|
| 493 |
"lambda_advisory": lam, "lambda_axes": axis,
|
| 494 |
"lambda_status": DOCTRINE["lambda"],
|
| 495 |
"below_advisory_floor": below_floor,
|
|
|
|
| 513 |
"experts": experts_out,
|
| 514 |
"all_expert_scores": [{"expert_id": s["expert"]["id"],
|
| 515 |
"score": round(s["score"], 4),
|
| 516 |
+
"matched": s["matched"],
|
| 517 |
+
"selection_basis": s.get("selection_basis")}
|
| 518 |
+
for s in scored],
|
| 519 |
+
"routing_evidence_state": "HEURISTIC",
|
| 520 |
+
"routing_limits": (
|
| 521 |
+
"Deterministic keyword routing with an explicit governance-first "
|
| 522 |
+
"fallback; not learned, not a quality guarantee, and not a substitute "
|
| 523 |
+
"for human approval."
|
| 524 |
+
),
|
| 525 |
"overall_lambda_advisory": overall_lambda,
|
| 526 |
"thompson_posteriors": _thompson_view(),
|
| 527 |
"doctrine": DOCTRINE,
|
|
|
|
| 941 |
"one_liner": ("SZL-Nemo — a sovereign, governed, self-improving AGENT model "
|
| 942 |
"built ON an open base (default Qwen3-32B, Apache-2.0)."),
|
| 943 |
"base": NEMO_BASE,
|
| 944 |
+
"hub_artifact": NEMO_ARTIFACT,
|
| 945 |
"what_is_ours": [
|
| 946 |
"Governed-MoE DOMAIN-EXPERT router (Λ-governed, signed every selection) — the differentiator.",
|
| 947 |
"MTP / speculative decoding as the inference default (app-layer; box ROADMAP→Forge).",
|
serve.py
CHANGED
|
@@ -2489,7 +2489,7 @@ try:
|
|
| 2489 |
return None
|
| 2490 |
|
| 2491 |
@app.get("/api/a11oy/v1/readiness/tab-matrix")
|
| 2492 |
-
async def _a11oy_readiness_tab_matrix(
|
| 2493 |
matrix = _rd_load((
|
| 2494 |
_rd_os.environ.get("SZL_TAB_MATRIX_PATH", ""),
|
| 2495 |
_rd_os.path.join(_RD_HARNESS_DIR, "tabs.json"),
|
|
@@ -2504,50 +2504,24 @@ try:
|
|
| 2504 |
"layer": "a11oy readiness tab-matrix",
|
| 2505 |
"honest": True,
|
| 2506 |
"available": False,
|
| 2507 |
-
"state": "UNAVAILABLE",
|
| 2508 |
-
"contract_version": None,
|
| 2509 |
"note": ("tabs.json not bundled with this deploy; generate it with "
|
| 2510 |
"tools/readiness-harness/gen_tabs_matrix.py"),
|
| 2511 |
"checked_at": _now,
|
| 2512 |
-
}, status_code=200
|
| 2513 |
-
"Cache-Control": "no-store",
|
| 2514 |
-
"X-A11OY-Data-State": "UNAVAILABLE",
|
| 2515 |
-
})
|
| 2516 |
if verdict is not None:
|
| 2517 |
verdict = dict(verdict)
|
| 2518 |
verdict["available"] = True
|
| 2519 |
-
|
| 2520 |
-
"version": matrix.get("version"),
|
| 2521 |
-
"doctrine": matrix.get("doctrine"),
|
| 2522 |
-
"organ": matrix.get("organ"),
|
| 2523 |
-
"generatedAt": matrix.get("generatedAt"),
|
| 2524 |
-
"summary": matrix.get("summary", {}),
|
| 2525 |
-
"stateVocabulary": matrix.get("stateVocabulary", {}),
|
| 2526 |
-
}
|
| 2527 |
-
_payload = {
|
| 2528 |
"layer": "a11oy readiness tab-matrix",
|
| 2529 |
"honest": True,
|
| 2530 |
"available": True,
|
| 2531 |
-
|
| 2532 |
-
# but that must not be presented as proof that every referenced
|
| 2533 |
-
# organ or tab is live.
|
| 2534 |
-
"state": "SNAPSHOT",
|
| 2535 |
-
"transport_state": "REACHABLE",
|
| 2536 |
-
"evidence_state": "SNAPSHOT",
|
| 2537 |
-
"contract_version": matrix.get("version"),
|
| 2538 |
-
"summary": _matrix_summary,
|
| 2539 |
-
"matrix": matrix if view != "summary" else None,
|
| 2540 |
"verdict": verdict or {
|
| 2541 |
"available": False,
|
| 2542 |
"note": "probe not yet run on this deploy (no readiness-verdict.json)",
|
| 2543 |
},
|
| 2544 |
"checked_at": _now,
|
| 2545 |
-
}
|
| 2546 |
-
return _RDJSON(_payload, status_code=200, headers={
|
| 2547 |
-
"Cache-Control": "no-store",
|
| 2548 |
-
"X-A11OY-Data-State": "SNAPSHOT",
|
| 2549 |
-
"X-A11OY-Transport-State": "REACHABLE",
|
| 2550 |
-
})
|
| 2551 |
print("[a11oy] Readiness tab-matrix registered: /api/a11oy/v1/readiness/tab-matrix",
|
| 2552 |
file=__import__("sys").stderr)
|
| 2553 |
except Exception as _rd_tm_e: # pragma: no cover
|
|
@@ -8046,6 +8020,13 @@ def _a11oy_sign_receipt(payload_obj) -> dict:
|
|
| 8046 |
return env
|
| 8047 |
|
| 8048 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8049 |
def _a11oy_pubkey_fpr() -> str:
|
| 8050 |
if not _A11OY_PUB_PEM:
|
| 8051 |
return "—"
|
|
@@ -11606,6 +11587,11 @@ try:
|
|
| 11606 |
def _a11oy_loop_pubpem():
|
| 11607 |
return _A11OY_PUB_PEM or ""
|
| 11608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11609 |
_loop_status = _szl_loop.register(
|
| 11610 |
app, "a11oy",
|
| 11611 |
_a11oy_sign_receipt,
|
|
|
|
| 2489 |
return None
|
| 2490 |
|
| 2491 |
@app.get("/api/a11oy/v1/readiness/tab-matrix")
|
| 2492 |
+
async def _a11oy_readiness_tab_matrix(): # noqa: ANN202
|
| 2493 |
matrix = _rd_load((
|
| 2494 |
_rd_os.environ.get("SZL_TAB_MATRIX_PATH", ""),
|
| 2495 |
_rd_os.path.join(_RD_HARNESS_DIR, "tabs.json"),
|
|
|
|
| 2504 |
"layer": "a11oy readiness tab-matrix",
|
| 2505 |
"honest": True,
|
| 2506 |
"available": False,
|
|
|
|
|
|
|
| 2507 |
"note": ("tabs.json not bundled with this deploy; generate it with "
|
| 2508 |
"tools/readiness-harness/gen_tabs_matrix.py"),
|
| 2509 |
"checked_at": _now,
|
| 2510 |
+
}, status_code=200)
|
|
|
|
|
|
|
|
|
|
| 2511 |
if verdict is not None:
|
| 2512 |
verdict = dict(verdict)
|
| 2513 |
verdict["available"] = True
|
| 2514 |
+
return _RDJSON({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2515 |
"layer": "a11oy readiness tab-matrix",
|
| 2516 |
"honest": True,
|
| 2517 |
"available": True,
|
| 2518 |
+
"matrix": matrix,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2519 |
"verdict": verdict or {
|
| 2520 |
"available": False,
|
| 2521 |
"note": "probe not yet run on this deploy (no readiness-verdict.json)",
|
| 2522 |
},
|
| 2523 |
"checked_at": _now,
|
| 2524 |
+
}, status_code=200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2525 |
print("[a11oy] Readiness tab-matrix registered: /api/a11oy/v1/readiness/tab-matrix",
|
| 2526 |
file=__import__("sys").stderr)
|
| 2527 |
except Exception as _rd_tm_e: # pragma: no cover
|
|
|
|
| 8020 |
return env
|
| 8021 |
|
| 8022 |
|
| 8023 |
+
# Ayllu registers earlier in this module, before the ephemeral signer exists.
|
| 8024 |
+
# Expose the signer through app.state so its request handlers can resolve it
|
| 8025 |
+
# lazily after startup. This removes the previous always-UNSIGNED Council path
|
| 8026 |
+
# without re-registering duplicate routes or committing any key material.
|
| 8027 |
+
app.state.szl_sign_receipt = _a11oy_sign_receipt
|
| 8028 |
+
|
| 8029 |
+
|
| 8030 |
def _a11oy_pubkey_fpr() -> str:
|
| 8031 |
if not _A11OY_PUB_PEM:
|
| 8032 |
return "—"
|
|
|
|
| 11587 |
def _a11oy_loop_pubpem():
|
| 11588 |
return _A11OY_PUB_PEM or ""
|
| 11589 |
|
| 11590 |
+
# Public verifier routes are registered earlier; resolve this callback from
|
| 11591 |
+
# app.state at request time so per-boot A11OY receipts verify against the
|
| 11592 |
+
# matching /cosign.pub key instead of the unrelated organization key.
|
| 11593 |
+
app.state.szl_verify_receipt = _a11oy_loop_verify
|
| 11594 |
+
|
| 11595 |
_loop_status = _szl_loop.register(
|
| 11596 |
app, "a11oy",
|
| 11597 |
_a11oy_sign_receipt,
|
szl_public_verify.py
CHANGED
|
@@ -149,7 +149,7 @@ def _norm_digest(v: Optional[str]) -> Optional[str]:
|
|
| 149 |
# ---------------------------------------------------------------------------
|
| 150 |
# Check 1 — signature (reuse szl_dsse.verify_envelope; honest labels)
|
| 151 |
# ---------------------------------------------------------------------------
|
| 152 |
-
def _check_signature(env: dict[str, Any]) -> dict[str, Any]:
|
| 153 |
out: dict[str, Any] = {"check": "signature",
|
| 154 |
"algo": "ECDSA-P256-SHA256 over DSSE PAE"}
|
| 155 |
try:
|
|
@@ -170,6 +170,28 @@ def _check_signature(env: dict[str, Any]) -> dict[str, Any]:
|
|
| 170 |
return {**out, "status": UNSIGNED_LOCAL,
|
| 171 |
"detail": ("envelope has no signatures[]; nothing to verify "
|
| 172 |
"(honest UNSIGNED-LOCAL — no signature fabricated)")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
try:
|
| 174 |
verdict = szl_dsse.verify_envelope(env)
|
| 175 |
except Exception as e:
|
|
@@ -330,7 +352,7 @@ def _overall(checks: list[dict[str, Any]]) -> str:
|
|
| 330 |
|
| 331 |
|
| 332 |
def verify_receipt(envelope: Any = None, receipt_id: Optional[str] = None,
|
| 333 |
-
organ: Optional[str] = None) -> dict[str, Any]:
|
| 334 |
"""Public verify. Accepts a DSSE envelope OR a receipt_id (chain lookup).
|
| 335 |
|
| 336 |
Returns a structured, honestly-labelled verdict. NEVER raises into the request
|
|
@@ -392,7 +414,7 @@ def verify_receipt(envelope: Any = None, receipt_id: Optional[str] = None,
|
|
| 392 |
rid = (str(receipt_id).strip().lower() if receipt_id
|
| 393 |
else _receipt_id_from_env(env, payload_obj))
|
| 394 |
|
| 395 |
-
sig = _check_signature(env)
|
| 396 |
dig = _check_payload_digest(env)
|
| 397 |
chain = _check_hash_chain(rid, organ)
|
| 398 |
checks = [sig, dig, chain]
|
|
@@ -425,6 +447,30 @@ def _shareable_link(env: dict[str, Any], rid: Optional[str]) -> dict[str, Any]:
|
|
| 425 |
# Registered BEFORE the SPA catch-all (mirrors szl_khipu_verify).
|
| 426 |
# ---------------------------------------------------------------------------
|
| 427 |
def register(app, ns: str = "a11oy") -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
async def _verify_post(request: Request): # noqa: ANN202
|
| 429 |
try:
|
| 430 |
body = await request.json()
|
|
@@ -435,7 +481,9 @@ def register(app, ns: str = "a11oy") -> dict:
|
|
| 435 |
envelope = body.get("envelope")
|
| 436 |
receipt_id = body.get("receipt_id") or body.get("receipt")
|
| 437 |
organ = body.get("organ")
|
| 438 |
-
|
|
|
|
|
|
|
| 439 |
status = 200 if result.get("ok") else 400
|
| 440 |
return JSONResponse(result, status_code=status,
|
| 441 |
headers={"x-szl-verify-verdict": str(result.get("verdict", ""))})
|
|
@@ -445,14 +493,23 @@ def register(app, ns: str = "a11oy") -> dict:
|
|
| 445 |
return JSONResponse(result, status_code=200,
|
| 446 |
headers={"x-szl-verify-verdict": str(result.get("verdict", ""))})
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
prefixes = [f"/api/{ns}/v1/verify", "/v1/verify"]
|
| 449 |
routes: list[str] = []
|
| 450 |
for p in prefixes:
|
|
|
|
|
|
|
| 451 |
app.add_api_route(f"{p}/receipt", _verify_post, methods=["POST"],
|
| 452 |
include_in_schema=True)
|
| 453 |
app.add_api_route(f"{p}/receipt/{{receipt_id}}", _verify_get_path,
|
| 454 |
methods=["GET"], include_in_schema=True)
|
| 455 |
-
routes.extend([f"{p}/receipt (
|
|
|
|
|
|
|
|
|
|
| 456 |
print(f"[{ns}] szl_public_verify routes registered "
|
| 457 |
f"(PUBLIC verify-a-receipt, {len(routes)} routes)", flush=True)
|
| 458 |
return {"ok": True, "ns": ns, "routes": routes}
|
|
|
|
| 149 |
# ---------------------------------------------------------------------------
|
| 150 |
# Check 1 — signature (reuse szl_dsse.verify_envelope; honest labels)
|
| 151 |
# ---------------------------------------------------------------------------
|
| 152 |
+
def _check_signature(env: dict[str, Any], runtime_verify_fn=None) -> dict[str, Any]:
|
| 153 |
out: dict[str, Any] = {"check": "signature",
|
| 154 |
"algo": "ECDSA-P256-SHA256 over DSSE PAE"}
|
| 155 |
try:
|
|
|
|
| 170 |
return {**out, "status": UNSIGNED_LOCAL,
|
| 171 |
"detail": ("envelope has no signatures[]; nothing to verify "
|
| 172 |
"(honest UNSIGNED-LOCAL — no signature fabricated)")}
|
| 173 |
+
keyids = [str(s.get("keyid") or "") for s in sigs if isinstance(s, dict)]
|
| 174 |
+
if "a11oy-inimage-ecdsa-p256" in keyids:
|
| 175 |
+
# Live Nemo/Council receipts use the per-boot key served at
|
| 176 |
+
# /cosign.pub, not the long-lived organization key embedded in
|
| 177 |
+
# szl_dsse. The old static-key-only path falsely marked them MISMATCH.
|
| 178 |
+
if not callable(runtime_verify_fn):
|
| 179 |
+
return {**out, "status": UNAVAILABLE,
|
| 180 |
+
"verify_key_url": "/cosign.pub",
|
| 181 |
+
"keyid_expected": "a11oy-inimage-ecdsa-p256",
|
| 182 |
+
"detail": "in-image verifier callback unavailable in this context"}
|
| 183 |
+
try:
|
| 184 |
+
runtime_verdict = runtime_verify_fn(env)
|
| 185 |
+
except Exception as e:
|
| 186 |
+
return {**out, "status": UNAVAILABLE,
|
| 187 |
+
"verify_key_url": "/cosign.pub",
|
| 188 |
+
"detail": f"in-image verify error: {type(e).__name__}"}
|
| 189 |
+
return {**out,
|
| 190 |
+
"status": (VERIFIED if runtime_verdict.get("signature_valid")
|
| 191 |
+
else MISMATCH),
|
| 192 |
+
"verify_key_url": "/cosign.pub",
|
| 193 |
+
"keyid_expected": "a11oy-inimage-ecdsa-p256",
|
| 194 |
+
"detail": runtime_verdict.get("detail")}
|
| 195 |
try:
|
| 196 |
verdict = szl_dsse.verify_envelope(env)
|
| 197 |
except Exception as e:
|
|
|
|
| 352 |
|
| 353 |
|
| 354 |
def verify_receipt(envelope: Any = None, receipt_id: Optional[str] = None,
|
| 355 |
+
organ: Optional[str] = None, runtime_verify_fn=None) -> dict[str, Any]:
|
| 356 |
"""Public verify. Accepts a DSSE envelope OR a receipt_id (chain lookup).
|
| 357 |
|
| 358 |
Returns a structured, honestly-labelled verdict. NEVER raises into the request
|
|
|
|
| 414 |
rid = (str(receipt_id).strip().lower() if receipt_id
|
| 415 |
else _receipt_id_from_env(env, payload_obj))
|
| 416 |
|
| 417 |
+
sig = _check_signature(env, runtime_verify_fn=runtime_verify_fn)
|
| 418 |
dig = _check_payload_digest(env)
|
| 419 |
chain = _check_hash_chain(rid, organ)
|
| 420 |
checks = [sig, dig, chain]
|
|
|
|
| 447 |
# Registered BEFORE the SPA catch-all (mirrors szl_khipu_verify).
|
| 448 |
# ---------------------------------------------------------------------------
|
| 449 |
def register(app, ns: str = "a11oy") -> dict:
|
| 450 |
+
async def _verify_manifest(): # noqa: ANN202
|
| 451 |
+
return JSONResponse({
|
| 452 |
+
"schema": "szl.public-receipt-verifier/manifest/v1",
|
| 453 |
+
"state": "LIVE",
|
| 454 |
+
"purpose": "Independently verify a pasted DSSE receipt or receipt id.",
|
| 455 |
+
"try": {
|
| 456 |
+
"method": "POST",
|
| 457 |
+
"endpoint": f"/api/{ns}/v1/verify/receipt",
|
| 458 |
+
"body": {"envelope": {"payloadType": "...", "payload": "...",
|
| 459 |
+
"signatures": []}},
|
| 460 |
+
},
|
| 461 |
+
"evidence": ["signature", "payload_digest", "hash_chain"],
|
| 462 |
+
"limits": (
|
| 463 |
+
"PASS means every check that could run passed and none mismatched; "
|
| 464 |
+
"it does not prove the model output is factually correct."
|
| 465 |
+
),
|
| 466 |
+
"reproduce": {
|
| 467 |
+
"public_key": "/cosign.pub",
|
| 468 |
+
"human_ui": "/verify",
|
| 469 |
+
"shareable_get": f"/api/{ns}/v1/verify/receipt/{{receipt_id}}",
|
| 470 |
+
},
|
| 471 |
+
"doctrine": _DOCTRINE,
|
| 472 |
+
})
|
| 473 |
+
|
| 474 |
async def _verify_post(request: Request): # noqa: ANN202
|
| 475 |
try:
|
| 476 |
body = await request.json()
|
|
|
|
| 481 |
envelope = body.get("envelope")
|
| 482 |
receipt_id = body.get("receipt_id") or body.get("receipt")
|
| 483 |
organ = body.get("organ")
|
| 484 |
+
runtime_verify = getattr(request.app.state, "szl_verify_receipt", None)
|
| 485 |
+
result = verify_receipt(envelope=envelope, receipt_id=receipt_id, organ=organ,
|
| 486 |
+
runtime_verify_fn=runtime_verify)
|
| 487 |
status = 200 if result.get("ok") else 400
|
| 488 |
return JSONResponse(result, status_code=status,
|
| 489 |
headers={"x-szl-verify-verdict": str(result.get("verdict", ""))})
|
|
|
|
| 493 |
return JSONResponse(result, status_code=200,
|
| 494 |
headers={"x-szl-verify-verdict": str(result.get("verdict", ""))})
|
| 495 |
|
| 496 |
+
# Capture the pre-existing routes because callers may register this module
|
| 497 |
+
# after a generic /api/{ns}/{path:path} proxy. We front-move the exact
|
| 498 |
+
# verifier routes after creation so ordered Starlette matching is stable.
|
| 499 |
+
existing_routes = list(app.router.routes)
|
| 500 |
prefixes = [f"/api/{ns}/v1/verify", "/v1/verify"]
|
| 501 |
routes: list[str] = []
|
| 502 |
for p in prefixes:
|
| 503 |
+
app.add_api_route(f"{p}/receipt", _verify_manifest, methods=["GET"],
|
| 504 |
+
include_in_schema=True)
|
| 505 |
app.add_api_route(f"{p}/receipt", _verify_post, methods=["POST"],
|
| 506 |
include_in_schema=True)
|
| 507 |
app.add_api_route(f"{p}/receipt/{{receipt_id}}", _verify_get_path,
|
| 508 |
methods=["GET"], include_in_schema=True)
|
| 509 |
+
routes.extend([f"{p}/receipt (GET manifest)", f"{p}/receipt (POST)",
|
| 510 |
+
f"{p}/receipt/{{receipt_id}} (GET)"])
|
| 511 |
+
added_routes = list(app.router.routes[len(existing_routes):])
|
| 512 |
+
app.router.routes[:] = added_routes + existing_routes
|
| 513 |
print(f"[{ns}] szl_public_verify routes registered "
|
| 514 |
f"(PUBLIC verify-a-receipt, {len(routes)} routes)", flush=True)
|
| 515 |
return {"ok": True, "ns": ns, "routes": routes}
|
test/test_council_contract.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.testclient import TestClient
|
| 5 |
+
|
| 6 |
+
import a11oy_ayllu as ayllu
|
| 7 |
+
import a11oy_nemo_core as nemo
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CouncilContractTests(unittest.TestCase):
|
| 11 |
+
def _result(self):
|
| 12 |
+
return {
|
| 13 |
+
"participants": ["Amaru", "Yupaq"],
|
| 14 |
+
"mode": "single-round",
|
| 15 |
+
"rounds": [
|
| 16 |
+
{"persona": "Amaru", "round": 1, "model": "test-model",
|
| 17 |
+
"stub": False, "answer": "Architecture proposal."},
|
| 18 |
+
{"persona": "Yupaq", "round": 1, "model": "test-model",
|
| 19 |
+
"stub": False, "answer": "Evidence gap."},
|
| 20 |
+
],
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
def test_generic_nemo_route_is_governance_first(self):
|
| 24 |
+
route = nemo.govern_route("Review a high-consequence agent action", top_k=2)
|
| 25 |
+
self.assertEqual(route["experts_selected"], ["governance", "code"])
|
| 26 |
+
self.assertEqual(route["routing_evidence_state"], "HEURISTIC")
|
| 27 |
+
self.assertEqual(route["experts"][0]["selection_basis"],
|
| 28 |
+
"explicit-governance-first-fallback")
|
| 29 |
+
|
| 30 |
+
def test_contract_is_proposal_only_and_replay_is_deterministic(self):
|
| 31 |
+
routing = {"state": "HEURISTIC", "experts_selected": ["governance"]}
|
| 32 |
+
one = ayllu._build_council_contract("review this", self._result(), routing)
|
| 33 |
+
two = ayllu._build_council_contract("review this", self._result(), routing)
|
| 34 |
+
self.assertEqual(one["schema"], ayllu.COUNCIL_SCHEMA)
|
| 35 |
+
self.assertEqual(one["decision_state"], "PROPOSAL_ONLY")
|
| 36 |
+
self.assertEqual(one["semantic_consensus"]["state"], "NOT_MEASURED")
|
| 37 |
+
self.assertTrue(one["human_checkpoint"]["required"])
|
| 38 |
+
self.assertEqual(one["evidence_state"], "LIVE")
|
| 39 |
+
self.assertEqual(one["replay"]["key"], two["replay"]["key"])
|
| 40 |
+
|
| 41 |
+
def test_runtime_signer_is_preferred(self):
|
| 42 |
+
def signer(payload):
|
| 43 |
+
return {"signed": True, "payload": "encoded", "signatures": [{"sig": "x"}]}
|
| 44 |
+
|
| 45 |
+
receipt = ayllu._make_receipt({"decision": "proposal"}, sign_fn=signer)
|
| 46 |
+
self.assertTrue(receipt["signed"])
|
| 47 |
+
self.assertEqual(receipt["payload"], "encoded")
|
| 48 |
+
|
| 49 |
+
def test_manifest_discloses_no_trained_weights(self):
|
| 50 |
+
manifest = ayllu.council_manifest()
|
| 51 |
+
self.assertFalse(manifest["nemo"]["weights_present"])
|
| 52 |
+
self.assertEqual(manifest["nemo"]["training_state"], "NOT_PERFORMED")
|
| 53 |
+
self.assertEqual(manifest["limits"]["decision_state"], "PROPOSAL_ONLY")
|
| 54 |
+
|
| 55 |
+
def test_http_council_emits_signed_contract(self):
|
| 56 |
+
app = FastAPI()
|
| 57 |
+
app.state.szl_sign_receipt = lambda payload: {
|
| 58 |
+
"signed": True, "payload": "encoded", "signatures": [{"sig": "test"}]}
|
| 59 |
+
ayllu.register(app)
|
| 60 |
+
|
| 61 |
+
async def fake_complete(system, prompt, **kwargs):
|
| 62 |
+
return {"text": "bounded answer", "model": "test-model", "stub": False}
|
| 63 |
+
|
| 64 |
+
original = ayllu._backend.model_complete
|
| 65 |
+
ayllu._backend.model_complete = fake_complete
|
| 66 |
+
try:
|
| 67 |
+
response = TestClient(app).post(
|
| 68 |
+
"/api/a11oy/v1/ayllu/council",
|
| 69 |
+
json={"prompt": "Review this proposal", "personas": ["Amaru"]})
|
| 70 |
+
finally:
|
| 71 |
+
ayllu._backend.model_complete = original
|
| 72 |
+
self.assertEqual(response.status_code, 200)
|
| 73 |
+
body = response.json()
|
| 74 |
+
self.assertEqual(body["contract"]["decision_state"], "PROPOSAL_ONLY")
|
| 75 |
+
self.assertTrue(body["receipt"]["signed"])
|
| 76 |
+
self.assertEqual(body["contract"]["routing"]["state"], "HEURISTIC")
|
| 77 |
+
self.assertEqual(body["contract"]["chain"]["state"], "LIVE")
|
| 78 |
+
self.assertTrue(body["contract"]["chain"]["chain_verified"])
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
unittest.main()
|
test/test_public_verify_routes.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import json
|
| 3 |
+
import unittest
|
| 4 |
+
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.testclient import TestClient
|
| 7 |
+
|
| 8 |
+
import szl_public_verify
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class PublicVerifyRouteTests(unittest.TestCase):
|
| 12 |
+
def setUp(self):
|
| 13 |
+
self.app = FastAPI()
|
| 14 |
+
|
| 15 |
+
@self.app.api_route("/api/a11oy/{path:path}", methods=["GET", "POST"])
|
| 16 |
+
async def proxy_catch_all(path: str):
|
| 17 |
+
return {"caught_by": "proxy", "path": path}
|
| 18 |
+
|
| 19 |
+
szl_public_verify.register(self.app, ns="a11oy")
|
| 20 |
+
self.client = TestClient(self.app)
|
| 21 |
+
|
| 22 |
+
def test_exact_routes_are_before_proxy(self):
|
| 23 |
+
paths = [getattr(route, "path", None) for route in self.app.router.routes]
|
| 24 |
+
self.assertLess(paths.index("/api/a11oy/v1/verify/receipt"),
|
| 25 |
+
paths.index("/api/a11oy/{path:path}"))
|
| 26 |
+
|
| 27 |
+
def test_get_exact_path_returns_manifest(self):
|
| 28 |
+
response = self.client.get("/api/a11oy/v1/verify/receipt")
|
| 29 |
+
self.assertEqual(response.status_code, 200)
|
| 30 |
+
self.assertEqual(response.json()["schema"],
|
| 31 |
+
"szl.public-receipt-verifier/manifest/v1")
|
| 32 |
+
|
| 33 |
+
def test_post_exact_path_reaches_verifier(self):
|
| 34 |
+
response = self.client.post("/api/a11oy/v1/verify/receipt", json={})
|
| 35 |
+
self.assertEqual(response.status_code, 400)
|
| 36 |
+
self.assertNotIn("caught_by", response.json())
|
| 37 |
+
|
| 38 |
+
def test_in_image_receipt_uses_runtime_verifier(self):
|
| 39 |
+
self.app.state.szl_verify_receipt = lambda envelope: {
|
| 40 |
+
"signature_valid": True, "detail": "verified by test runtime key"}
|
| 41 |
+
payload = base64.b64encode(json.dumps({"decision": "proposal"}).encode()).decode()
|
| 42 |
+
envelope = {
|
| 43 |
+
"payloadType": "application/vnd.szl.receipt+json",
|
| 44 |
+
"payload": payload,
|
| 45 |
+
"signatures": [{"keyid": "a11oy-inimage-ecdsa-p256", "sig": "test"}],
|
| 46 |
+
}
|
| 47 |
+
response = self.client.post("/api/a11oy/v1/verify/receipt",
|
| 48 |
+
json={"envelope": envelope})
|
| 49 |
+
self.assertEqual(response.status_code, 200)
|
| 50 |
+
signature = response.json()["checks"][0]
|
| 51 |
+
self.assertEqual(signature["status"], "VERIFIED")
|
| 52 |
+
self.assertEqual(signature["verify_key_url"], "/cosign.pub")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
unittest.main()
|