feat: add evidence-bound Council v2 and verifier contract

#2
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
- return _dsse.sign(payload)
 
 
 
 
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
- let html=cap;
 
 
 
 
 
 
 
 
 
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 &amp; 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
- receipt = _make_receipt({
 
 
 
 
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 &amp; 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
- scored.sort(key=lambda r: (-r["score"], r["expert"]["id"]))
 
 
 
 
 
 
 
 
 
 
 
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"]} for s in scored],
 
 
 
 
 
 
 
 
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(view: str = "full"): # 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,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, headers={
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
- _matrix_summary = {
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
- # The contract is a shipped snapshot. The HTTP read is reachable,
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
- result = verify_receipt(envelope=envelope, receipt_id=receipt_id, organ=organ)
 
 
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 (POST)", f"{p}/receipt/{{receipt_id}} (GET)"])
 
 
 
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()