betterwithage commited on
Commit
4867bc1
·
verified ·
1 Parent(s): 8cf9c93

chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)

Browse files

Automated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, a11oy_ayllu.py, ayllu/backend.py, ayllu/loop.py, ayllu/lounge.py, research/__init__.py, research/a11oy_primary_project_registry.py, serve.py, szl3d_holographic.py, szl_be_hardening.py, szl_claim_rupture_gate.py, szl_hub.py, szl_waqay_security_loop.py
Deleted (gone from the repo + Dockerfile COPY set): (none)

Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.

Dockerfile CHANGED
@@ -163,6 +163,16 @@ COPY knowledge.json ./static/knowledge.json
163
  # registers live. (dockerfile-copy-guard verifies these sources exist on main.)
164
  COPY a11oy_ayllu.py ./
165
  COPY ayllu/ ./ayllu/
 
 
 
 
 
 
 
 
 
 
166
  # routers/ — Wave-K Dev4 serve.py decomposition (first bounded slice). serve.py
167
  # imports `from routers import lambda_bounty|research_3d|frontier_reads` (guarded)
168
  # and calls each register(app) BEFORE the SPA catch-all. This Dockerfile uses no
@@ -649,7 +659,9 @@ COPY benchmarks/pinn/run_bench.py ./benchmarks/pinn/run_bench.py
649
  # hf-sync mirrored) — same baked-only pattern as web/sda.html + web/immune.html;
650
  # declared in copy-sync-lockstep.json image_only_assets + hf-module-drift-allow.json
651
  # accepted_divergences.
652
- COPY web/formulas.html web/v4_fleet_panel.html web/operator.html web/fleet-c2.html web/living-anatomy.html web/nemo.html web/restraint.html web/restraint-bench.html web/holo.html web/constitution.html web/quant.html web/estate-hologram.html web/hologram.html web/signature-is-not-proof.html web/defense-readiness.html web/determinacy.html web/verify-receipt.html web/sda.html web/dns.html ./web/
 
 
653
  # ADDITIVE (Lane A AGENTIC CORE, Dev A, 2026-06-14; QA9 restore 2026-06): the
654
  # resumable ReAct agent-loop core module. Per-file COPY (this Dockerfile uses no
655
  # COPY . .). a11oy_react_core.py is imported by serve.py (try/except guarded) and
 
163
  # registers live. (dockerfile-copy-guard verifies these sources exist on main.)
164
  COPY a11oy_ayllu.py ./
165
  COPY ayllu/ ./ayllu/
166
+ # Waqay Security Loop wave 15: pure read-only proposal contract. The module
167
+ # exposes zero external effectors; serve.py registers only its manifest GET.
168
+ COPY szl_waqay_security_loop.py ./
169
+ # Claim-integrity Rupture Gate wave 15: contract-only, external signals only,
170
+ # unsigned deterministic receipts, zero effectors.
171
+ COPY szl_claim_rupture_gate.py ./
172
+ # Primary official project registry (51 records across 10 fields). Runtime
173
+ # serves the deterministic, unranked registry; optional live metadata remains a
174
+ # bounded adapter and is not executed on anonymous public requests.
175
+ COPY research/ ./research/
176
  # routers/ — Wave-K Dev4 serve.py decomposition (first bounded slice). serve.py
177
  # imports `from routers import lambda_bounty|research_3d|frontier_reads` (guarded)
178
  # and calls each register(app) BEFORE the SPA catch-all. This Dockerfile uses no
 
659
  # hf-sync mirrored) — same baked-only pattern as web/sda.html + web/immune.html;
660
  # declared in copy-sync-lockstep.json image_only_assets + hf-module-drift-allow.json
661
  # accepted_divergences.
662
+ COPY web/formulas.html web/v4_fleet_panel.html web/operator.html web/fleet-c2.html web/living-anatomy.html web/nemo.html web/restraint.html web/restraint-bench.html web/holo.html web/constitution.html web/quant.html web/estate-hologram.html web/hologram.html web/determinacy.html web/verify-receipt.html web/sda.html web/dns.html ./web/
663
+ COPY web/signature-is-not-proof.html ./web/signature-is-not-proof.html
664
+ COPY web/defense-readiness.html ./web/defense-readiness.html
665
  # ADDITIVE (Lane A AGENTIC CORE, Dev A, 2026-06-14; QA9 restore 2026-06): the
666
  # resumable ReAct agent-loop core module. Per-file COPY (this Dockerfile uses no
667
  # COPY . .). a11oy_react_core.py is imported by serve.py (try/except guarded) and
a11oy_ayllu.py CHANGED
@@ -24,9 +24,11 @@ from __future__ import annotations
24
  import base64
25
  import hashlib
26
  import json
 
27
  import threading
28
  import time
29
  import uuid
 
30
  from typing import Any, Dict, Optional
31
 
32
  # FastAPI resolves endpoint annotations with get_type_hints against THIS module's
@@ -69,6 +71,10 @@ __version__ = _AYLLU_VERSION
69
  MAX_PROMPT_CHARS = 6000
70
  COUNCIL_MAX = 5 # hard cap on participants / call
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"
@@ -114,9 +120,26 @@ def _receipt_sha(receipt: Optional[dict]) -> Optional[str]:
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)
@@ -128,8 +151,9 @@ def _make_receipt(payload: Dict[str, Any], sign_fn=None) -> Dict[str, Any]:
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")
@@ -151,7 +175,82 @@ def _sha256_json(value: Any) -> str:
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 {
@@ -174,7 +273,7 @@ def council_manifest(ns: str = "a11oy") -> Dict[str, Any]:
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": {
@@ -182,10 +281,20 @@ def council_manifest(ns: str = "a11oy") -> Dict[str, Any]:
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",
@@ -251,6 +360,11 @@ def _build_council_contract(prompt: str, result: Dict[str, Any],
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")),
@@ -263,8 +377,11 @@ def _build_council_contract(prompt: str, result: Dict[str, Any],
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:
@@ -278,6 +395,7 @@ def _build_council_contract(prompt: str, result: Dict[str, Any],
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,
@@ -297,6 +415,10 @@ def _build_council_contract(prompt: str, result: Dict[str, Any],
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,
@@ -320,18 +442,51 @@ def _build_council_contract(prompt: str, result: Dict[str, Any],
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",
@@ -341,6 +496,7 @@ def _mint_council_chain(contract: Dict[str, Any], ns: str = "a11oy") -> Dict[str
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 {
@@ -349,6 +505,7 @@ def _mint_council_chain(contract: Dict[str, Any], ns: str = "a11oy") -> Dict[str
349
  "receipt_id": None,
350
  "error": type(exc).__name__,
351
  "honesty": "Khipu append unavailable; no chain receipt fabricated.",
 
352
  }
353
 
354
 
@@ -384,6 +541,7 @@ select[multiple]{height:auto}
384
  textarea{resize:vertical;margin-bottom:8px}
385
  button{background:var(--teal);color:#04140f;border:0;border-radius:7px;padding:8px 16px;
386
  font-weight:700;cursor:pointer}
 
387
  button.mini{background:transparent;color:var(--teal);border:1px solid var(--line);padding:3px 9px;
388
  font-weight:600;font-size:12px}
389
  .hint{color:var(--dim);font-size:12px;margin:0 0 8px}
@@ -432,6 +590,21 @@ section[id]{scroll-margin-top:72px}
432
  .tgl{display:flex;gap:7px;align-items:center;color:var(--dim);font-size:13px;margin:0 0 8px}
433
  .tgl input{width:auto}
434
  .prov{color:var(--dim);font-size:11px;margin-top:14px;line-height:1.6}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  </style></head><body>
436
  <header class="topbar"><div class="tb-wrap">
437
  <a class="tb-brand" href="/ayllu">Ayllu <span id="badge" class="badge">…</span></a>
@@ -446,13 +619,13 @@ section[id]{scroll-margin-top:72px}
446
  <section class="card" id="sec-ask">
447
  <h2>Ask a persona</h2>
448
  <div class="row">
449
- <select id="persona"></select>
450
  <input id="difficulty" type="number" min="0" max="1" step="0.1"
451
- placeholder="difficulty 0–1 (optional)">
452
  </div>
453
- <textarea id="askprompt" rows="3" placeholder="Ask a persona…"></textarea>
454
  <button id="askbtn">Ask</button>
455
- <div id="askout" class="out"></div>
456
  </section>
457
 
458
  <section class="card" id="sec-council">
@@ -465,12 +638,12 @@ section[id]{scroll-margin-top:72px}
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>
468
- <select id="councilsel" multiple size="6"></select>
469
  <label class="tgl"><input type="checkbox" id="debate">
470
  Debate mode — positions, then explicit dissent &amp; converge (2× cost)</label>
471
- <textarea id="councilprompt" rows="3" placeholder="A question for the council…"></textarea>
472
  <button id="councilbtn">Convene</button>
473
- <div id="councilout" class="out"></div>
474
  </section>
475
 
476
  <section class="card" id="sec-roster">
@@ -534,8 +707,12 @@ instilled knowledge (cited text in the system prompt); nothing here was "trained
534
  const NS="__NS__";
535
  const api = p => `/api/${NS}/v1/ayllu/`+p;
536
  const gapi = p => `/api/${NS}/v1/`+p;
537
- async function j(url,opts){const r=await fetch(url,opts);
538
- let d={};try{d=await r.json();}catch(e){}return {ok:r.ok,status:r.status,data:d};}
 
 
 
 
539
  function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,
540
  c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
541
  function hue(n){let h=0;for(const c of String(n))h=(h*31+c.charCodeAt(0))%360;return h;}
@@ -554,8 +731,14 @@ function renderTurn(t){
554
  + `<div class="ans">${ans}</div></div>`;
555
  }
556
  async function loadRoster(){
557
- const {ok,data}=await j(api('roster'));
558
- if(!ok)return;
 
 
 
 
 
 
559
  document.getElementById('count').textContent=data.count;
560
  const b=data.backend||{}, badge=document.getElementById('badge'), mode=b.mode||'?';
561
  badge.textContent=mode.toUpperCase();
@@ -575,15 +758,17 @@ async function loadRoster(){
575
  document.querySelector('#roster tbody').innerHTML=rows.join('');
576
  }
577
  document.getElementById('askbtn').onclick=async()=>{
 
578
  const persona=document.getElementById('persona').value;
579
  const prompt=document.getElementById('askprompt').value.trim();
580
  const d=document.getElementById('difficulty').value;
581
  const out=document.getElementById('askout');
582
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
583
- out.textContent='…thinking';
584
  const body={persona,prompt}; if(d!=='')body.difficulty=parseFloat(d);
585
  const {ok,status,data}=await j(api('ask'),{method:'POST',
586
  headers:{'content-type':'application/json'},body:JSON.stringify(body)});
 
587
  if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
588
  +(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
589
  const r=data.receipt||{}, sig=r.signed?'signed':'UNSIGNED';
@@ -591,15 +776,18 @@ document.getElementById('askbtn').onclick=async()=>{
591
  +`<div class="rcpt">receipt: ${sig} · ask ${esc(String(data.ask_id)).slice(0,8)}</div>`;
592
  };
593
  document.getElementById('councilbtn').onclick=async()=>{
 
594
  const prompt=document.getElementById('councilprompt').value.trim();
595
  const out=document.getElementById('councilout');
596
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
597
  const picks=[...document.getElementById('councilsel').selectedOptions].map(o=>o.value);
598
  const debate=document.getElementById('debate').checked;
599
  out.textContent=debate?'…convening (debate: 2 bounded rounds)':'…convening';
 
600
  const body={prompt}; if(picks.length)body.personas=picks; if(debate)body.debate=true;
601
  const {ok,status,data}=await j(api('council'),{method:'POST',
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||{};
@@ -607,10 +795,10 @@ document.getElementById('councilbtn').onclick=async()=>{
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>`;
@@ -706,6 +894,13 @@ def register(app, ns: str = "a11oy") -> str:
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:
@@ -727,7 +922,9 @@ def register(app, ns: str = "a11oy") -> str:
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()
@@ -763,7 +960,12 @@ def register(app, ns: str = "a11oy") -> str:
763
  return JSONResponse(
764
  {"error": "'difficulty' must be a number between 0 and 1"},
765
  status_code=422)
766
- turn = await run_turn(p, prompt, model_complete=_backend.model_complete,
 
 
 
 
 
767
  difficulty=difficulty)
768
  ask_id = str(uuid.uuid4())
769
  receipt = _make_receipt({
@@ -825,16 +1027,24 @@ def register(app, ns: str = "a11oy") -> str:
825
  return JSONResponse(
826
  {"error": "no known personas in request",
827
  "known": [x.name for x in ROSTER]}, status_code=422)
828
- result = await _LOUNGE.deliberate(prompt, personas,
829
- model_complete=_backend.model_complete,
830
- debate=debate)
 
 
 
 
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(),
@@ -886,5 +1096,7 @@ def register(app, ns: str = "a11oy") -> str:
886
  f"ok — ayllu registered: {len(ROSTER)} personas; live model backend "
887
  f"({_backend.backend_status().get('mode')}); bounded-autonomy Λ-gate; "
888
  f"/ayllu + /api/{ns}/v1/ayllu/roster|ask|council|lounge; "
889
- f"debate-mode council; version={__version__}"
 
 
890
  )
 
24
  import base64
25
  import hashlib
26
  import json
27
+ import os
28
  import threading
29
  import time
30
  import uuid
31
+ from pathlib import Path
32
  from typing import Any, Dict, Optional
33
 
34
  # FastAPI resolves endpoint annotations with get_type_hints against THIS module's
 
71
  MAX_PROMPT_CHARS = 6000
72
  COUNCIL_MAX = 5 # hard cap on participants / call
73
  COUNCIL_DEBATE_MAX = 3 # debate doubles model calls; tighter cap bounds cost
74
+ ASK_MAX_TOKENS = 384
75
+ ASK_TURN_TIMEOUT_S = 45.0
76
+ COUNCIL_MAX_TOKENS = 192
77
+ COUNCIL_TURN_TIMEOUT_S = 45.0
78
  COUNCIL_DEFAULT = ["Amaru", "Kamachiq", "Qhatuq"] # architect · orchestrator · markets
79
 
80
  COUNCIL_CONTRACT_VERSION = "2.0"
 
120
 
121
 
122
  def _make_receipt(payload: Dict[str, Any], sign_fn=None) -> Dict[str, Any]:
123
+ """Wrap payload in DSSE without overstating the signer's identity.
124
+
125
+ ``szl_dsse`` is the organization-key path: it signs only when an operator
126
+ injects the established Cosign private-key runtime secret. ``sign_fn`` is
127
+ the host's explicitly boot-ephemeral development signer. Prefer the former
128
+ only when its key is genuinely loadable, then fall back to the development
129
+ signer. With neither path available, emit an honest unsigned envelope.
130
+ """
131
  body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
132
  honesty = "UNSIGNED — szl_dsse not present; no signature fabricated."
133
+ if _dsse is not None:
134
+ try:
135
+ signing_available = getattr(_dsse, "signing_available", None)
136
+ if (callable(signing_available) and signing_available()
137
+ and hasattr(_dsse, "sign_payload")):
138
+ return _dsse.sign_payload(
139
+ payload, "application/vnd.szl.receipt+json")
140
+ except Exception as exc:
141
+ honesty = (f"UNSIGNED — organization-key signer unavailable "
142
+ f"({str(exc)[:80]}); no signature fabricated.")
143
  if callable(sign_fn):
144
  try:
145
  env = sign_fn(payload)
 
151
  "no signature fabricated.")
152
  if _dsse is not None:
153
  try:
154
+ # No organization key and no host development signer: use the
155
+ # canonical implementation only to construct its explicit UNSIGNED
156
+ # envelope (it never fabricates signature bytes).
157
  if hasattr(_dsse, "sign_payload"):
158
  return _dsse.sign_payload(
159
  payload, "application/vnd.szl.receipt+json")
 
175
  return hashlib.sha256(body).hexdigest()
176
 
177
 
178
+ def _council_store_path(ns: str) -> tuple[str, str]:
179
+ """Resolve Council state without claiming an unverified durable mount.
180
+
181
+ Operators can name an exact path or an established data directory. Local
182
+ development defaults to a gitignored directory beside this module, which
183
+ survives process restarts but is *not* claimed to survive a container or
184
+ Space rebuild.
185
+ """
186
+ exact = os.environ.get("A11OY_AYLLU_KHIPU_PATH")
187
+ if exact:
188
+ return exact, "A11OY_AYLLU_KHIPU_PATH"
189
+ data_dir = os.environ.get("A11OY_DATA_DIR")
190
+ if data_dir:
191
+ return os.path.join(data_dir, "ayllu", f"khipu_{ns}_council.sqlite3"), "A11OY_DATA_DIR"
192
+ khipu_dir = os.environ.get("SZL_KHIPU_DIR")
193
+ if khipu_dir:
194
+ return os.path.join(khipu_dir, f"khipu_{ns}_council.sqlite3"), "SZL_KHIPU_DIR"
195
+ return str(Path(__file__).resolve().parent / ".a11oy-state"
196
+ / f"khipu_{ns}_council.sqlite3"), "REPOSITORY_LOCAL_DEVELOPMENT_STATE"
197
+
198
+
199
+ def _open_council_store(ns: str):
200
+ """Open the repository's tested durable Khipu implementation.
201
+
202
+ Returns ``(store, metadata)``. Failure is explicit; the caller may still
203
+ use the legacy in-memory DAG but must report that downgrade.
204
+ """
205
+ path, configured_by = _council_store_path(ns)
206
+ try:
207
+ from szl_be_hardening import DurableKhipu
208
+ store = DurableKhipu("ayllu_council", ns=ns, path=path)
209
+ durable = store.backend in ("sqlite", "json")
210
+ meta = {
211
+ "backend": store.backend,
212
+ "durable": durable,
213
+ "configured_by": configured_by,
214
+ "survives_process_restart": durable,
215
+ "survives_redeploy": "NOT_VERIFIED",
216
+ "redeploy_requirement": (
217
+ "Mount the configured path on persistent storage; a writable "
218
+ "local/container filesystem alone does not prove redeploy persistence."
219
+ ),
220
+ }
221
+ return store, meta
222
+ except Exception as exc:
223
+ return None, {
224
+ "backend": "memory",
225
+ "durable": False,
226
+ "configured_by": configured_by,
227
+ "survives_process_restart": False,
228
+ "survives_redeploy": "NOT_VERIFIED",
229
+ "error": type(exc).__name__,
230
+ "honesty": "Durable Khipu unavailable; Council will use the in-process DAG.",
231
+ }
232
+
233
+
234
+ def _council_store_metadata(store, fallback: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
235
+ if store is None:
236
+ return dict(fallback or {
237
+ "backend": "memory", "durable": False,
238
+ "survives_process_restart": False,
239
+ "survives_redeploy": "NOT_VERIFIED",
240
+ })
241
+ backend = getattr(store, "backend", "memory")
242
+ base = dict(fallback or {})
243
+ base.update({
244
+ "backend": backend,
245
+ "durable": backend in ("sqlite", "json"),
246
+ "survives_process_restart": backend in ("sqlite", "json"),
247
+ "survives_redeploy": "NOT_VERIFIED",
248
+ })
249
+ return base
250
+
251
+
252
+ def council_manifest(ns: str = "a11oy",
253
+ chain_storage: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
254
  """Side-effect-free, investor-readable contract for the bounded council."""
255
  base = f"/api/{ns}/v1/ayllu"
256
  return {
 
273
  "persona/model/round/output digest per turn",
274
  "Nemo governed-route decision and DSSE receipt",
275
  "deterministic replay key over participants, mode, and output digests",
276
+ "Khipu chain receipt with runtime-reported storage backend and durability",
277
  "outer Council DSSE receipt",
278
  ],
279
  "limits": {
 
281
  "participants": COUNCIL_MAX,
282
  "debate_participants": COUNCIL_DEBATE_MAX,
283
  "debate_rounds": 2,
284
+ "ask_tokens_per_turn": ASK_MAX_TOKENS,
285
+ "ask_timeout_s": ASK_TURN_TIMEOUT_S,
286
+ "council_tokens_per_turn": COUNCIL_MAX_TOKENS,
287
+ "council_timeout_s": COUNCIL_TURN_TIMEOUT_S,
288
+ "round_fanout": "CONCURRENT_BOUNDED",
289
  "effectors": "none",
290
  "decision_state": "PROPOSAL_ONLY",
291
  "semantic_consensus": "NOT_MEASURED",
292
+ "chain_storage": chain_storage or {
293
+ "backend": "NOT_INSPECTED",
294
+ "durable": "NOT_INSPECTED",
295
+ "survives_process_restart": "NOT_INSPECTED",
296
+ "survives_redeploy": "NOT_VERIFIED",
297
+ },
298
  },
299
  "reproduce": {
300
  "manifest": base + "/council/manifest",
 
360
  "round": turn.get("round"),
361
  "model": turn.get("model"),
362
  "stub": bool(turn.get("stub")),
363
+ "timeout": bool(turn.get("timeout", False)),
364
+ "token_budget": turn.get("token_budget"),
365
+ "timeout_s": turn.get("timeout_s"),
366
+ "correctness_state": ("NOT_APPLICABLE_STUB" if bool(turn.get("stub"))
367
+ else "UNVERIFIED_MODEL_OUTPUT"),
368
  "output_sha256": (hashlib.sha256(str(answer).encode("utf-8")).hexdigest()
369
  if answer is not None else None),
370
  "energy_receipt_sha256": _receipt_sha(turn.get("energy_receipt")),
 
377
  "turns": turn_evidence,
378
  }
379
  live_turns = sum(1 for t in turn_evidence if not t["stub"])
380
+ timeout_turns = sum(1 for t in turn_evidence if t["timeout"])
381
  if not turn_evidence:
382
  evidence_state = "UNAVAILABLE"
383
+ elif timeout_turns and not live_turns:
384
+ evidence_state = "UNAVAILABLE"
385
  elif live_turns == len(turn_evidence):
386
  evidence_state = "LIVE"
387
  elif live_turns:
 
395
  "decision_state": "PROPOSAL_ONLY",
396
  "approval_state": "HUMAN_REVIEW_REQUIRED",
397
  "evidence_state": evidence_state,
398
+ "correctness_state": "NOT_VERIFIED",
399
  "prompt_sha256": replay_material["prompt_sha256"],
400
  "turn_evidence": turn_evidence,
401
  "routing": nemo_route,
 
415
  "participants_max": COUNCIL_MAX,
416
  "debate_participants_max": COUNCIL_DEBATE_MAX,
417
  "rounds_max": 2,
418
+ "tokens_per_turn_max": COUNCIL_MAX_TOKENS,
419
+ "turn_timeout_s": COUNCIL_TURN_TIMEOUT_S,
420
+ "round_fanout": "CONCURRENT_BOUNDED",
421
+ "timeout_turns": timeout_turns,
422
  "model_calls_observed": len(turn_evidence),
423
  "external_effectors": 0,
424
  "automatic_commit": False,
 
442
  }
443
 
444
 
445
+ def _mint_council_chain(contract: Dict[str, Any], ns: str = "a11oy",
446
+ store=None,
447
+ storage_meta: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
448
+ """Append the proposal receipt to durable Khipu, with honest fallback."""
449
+ payload = {
450
+ "contract_version": contract.get("contract_version"),
451
+ "decision_state": contract.get("decision_state"),
452
+ "evidence_state": contract.get("evidence_state"),
453
+ "prompt_sha256": contract.get("prompt_sha256"),
454
+ "replay_key": (contract.get("replay") or {}).get("key"),
455
+ }
456
+ if store is not None:
457
+ try:
458
+ receipt = store.emit("ayllu.council.proposal", payload)
459
+ ok, depth, first_break = store.verify()
460
+ meta = _council_store_metadata(store, storage_meta)
461
+ return {
462
+ "state": "LIVE",
463
+ "organ": "ayllu_council",
464
+ "receipt_id": receipt.get("digest"),
465
+ "seq": receipt.get("seq"),
466
+ "chain_verified": bool(ok),
467
+ "first_break_seq": first_break,
468
+ "depth": depth,
469
+ "persistence": ("PROCESS_RESTART_DURABLE_LOCAL_DISK"
470
+ if meta["durable"] else
471
+ "IN_MEMORY_RESETS_ON_RESTART"),
472
+ "storage": meta,
473
+ }
474
+ except Exception as exc:
475
+ # Do not lose the advisory response merely because durable storage
476
+ # failed. Fall through to the legacy in-process chain and report the
477
+ # exact downgrade in the returned evidence.
478
+ storage_meta = {
479
+ **(storage_meta or {}),
480
+ "backend": "memory",
481
+ "durable": False,
482
+ "survives_process_restart": False,
483
+ "survives_redeploy": "NOT_VERIFIED",
484
+ "durable_append_error": type(exc).__name__,
485
+ }
486
  try:
487
  import szl_khipu
488
  dag = szl_khipu.get_dag("ayllu_council", ns=ns)
489
+ receipt = dag.emit("ayllu.council.proposal", payload)
 
 
 
 
 
 
490
  chain = dag.verify_chain()
491
  return {
492
  "state": "LIVE",
 
496
  "chain_verified": bool(chain.get("ok")),
497
  "depth": dag.depth(),
498
  "persistence": "IN_MEMORY_RESETS_ON_RESTART",
499
+ "storage": _council_store_metadata(None, storage_meta),
500
  }
501
  except Exception as exc:
502
  return {
 
505
  "receipt_id": None,
506
  "error": type(exc).__name__,
507
  "honesty": "Khipu append unavailable; no chain receipt fabricated.",
508
+ "storage": _council_store_metadata(None, storage_meta),
509
  }
510
 
511
 
 
541
  textarea{resize:vertical;margin-bottom:8px}
542
  button{background:var(--teal);color:#04140f;border:0;border-radius:7px;padding:8px 16px;
543
  font-weight:700;cursor:pointer}
544
+ button:disabled{cursor:wait;opacity:.55}
545
  button.mini{background:transparent;color:var(--teal);border:1px solid var(--line);padding:3px 9px;
546
  font-weight:600;font-size:12px}
547
  .hint{color:var(--dim);font-size:12px;margin:0 0 8px}
 
590
  .tgl{display:flex;gap:7px;align-items:center;color:var(--dim);font-size:13px;margin:0 0 8px}
591
  .tgl input{width:auto}
592
  .prov{color:var(--dim);font-size:11px;margin-top:14px;line-height:1.6}
593
+ @media (max-width:720px){
594
+ main{padding:24px 14px}
595
+ .tb-wrap{padding:9px 14px;flex-wrap:nowrap}
596
+ .tb-brand{flex:0 0 auto}
597
+ .tb-nav{flex:1 1 auto;min-width:0;flex-wrap:nowrap;overflow-x:auto;
598
+ -webkit-overflow-scrolling:touch;scrollbar-width:none}
599
+ .tb-nav::-webkit-scrollbar{display:none}
600
+ .tb-nav a{flex:0 0 auto}
601
+ .card{padding:14px}
602
+ .row{display:grid;grid-template-columns:minmax(0,1fr)}
603
+ .row select,.row input{width:100%;min-width:0}
604
+ table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;white-space:nowrap}
605
+ .meta{margin-left:0;width:100%}
606
+ section[id]{scroll-margin-top:58px}
607
+ }
608
  </style></head><body>
609
  <header class="topbar"><div class="tb-wrap">
610
  <a class="tb-brand" href="/ayllu">Ayllu <span id="badge" class="badge">…</span></a>
 
619
  <section class="card" id="sec-ask">
620
  <h2>Ask a persona</h2>
621
  <div class="row">
622
+ <select id="persona" aria-label="Persona"></select>
623
  <input id="difficulty" type="number" min="0" max="1" step="0.1"
624
+ placeholder="difficulty 0–1 (optional)" aria-label="Difficulty from zero to one">
625
  </div>
626
+ <textarea id="askprompt" rows="3" placeholder="Ask a persona…" aria-label="Prompt for the selected persona"></textarea>
627
  <button id="askbtn">Ask</button>
628
+ <div id="askout" class="out" aria-live="polite"></div>
629
  </section>
630
 
631
  <section class="card" id="sec-council">
 
638
  <p class="hint">Defaults to 3 core personas; select up to 5 (⌘/Ctrl-click). Fan-out is
639
  capped to protect cost. Debate mode runs exactly two bounded rounds
640
  (after arXiv:2305.14325) and is capped to 3 personas.</p>
641
+ <select id="councilsel" multiple size="6" aria-label="Council personas"></select>
642
  <label class="tgl"><input type="checkbox" id="debate">
643
  Debate mode — positions, then explicit dissent &amp; converge (2× cost)</label>
644
+ <textarea id="councilprompt" rows="3" placeholder="A question for the council…" aria-label="Question for the council"></textarea>
645
  <button id="councilbtn">Convene</button>
646
+ <div id="councilout" class="out" aria-live="polite"></div>
647
  </section>
648
 
649
  <section class="card" id="sec-roster">
 
707
  const NS="__NS__";
708
  const api = p => `/api/${NS}/v1/ayllu/`+p;
709
  const gapi = p => `/api/${NS}/v1/`+p;
710
+ async function j(url,opts){
711
+ try{const r=await fetch(url,opts);let d={};
712
+ try{d=await r.json();}catch(e){}
713
+ return {ok:r.ok,status:r.status,data:d};
714
+ }catch(e){return {ok:false,status:'network',data:{error:'request unavailable'}};}
715
+ }
716
  function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,
717
  c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
718
  function hue(n){let h=0;for(const c of String(n))h=(h*31+c.charCodeAt(0))%360;return h;}
 
731
  + `<div class="ans">${ans}</div></div>`;
732
  }
733
  async function loadRoster(){
734
+ const {ok,status,data}=await j(api('roster'));
735
+ if(!ok){
736
+ document.getElementById('count').textContent='—';
737
+ const badge=document.getElementById('badge');
738
+ badge.textContent='UNAVAILABLE';badge.className='badge warn';
739
+ badge.title='roster endpoint unavailable ('+String(status)+') — no live state fabricated';
740
+ return;
741
+ }
742
  document.getElementById('count').textContent=data.count;
743
  const b=data.backend||{}, badge=document.getElementById('badge'), mode=b.mode||'?';
744
  badge.textContent=mode.toUpperCase();
 
758
  document.querySelector('#roster tbody').innerHTML=rows.join('');
759
  }
760
  document.getElementById('askbtn').onclick=async()=>{
761
+ const btn=document.getElementById('askbtn');
762
  const persona=document.getElementById('persona').value;
763
  const prompt=document.getElementById('askprompt').value.trim();
764
  const d=document.getElementById('difficulty').value;
765
  const out=document.getElementById('askout');
766
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
767
+ out.textContent='…thinking';btn.disabled=true;out.setAttribute('aria-busy','true');
768
  const body={persona,prompt}; if(d!=='')body.difficulty=parseFloat(d);
769
  const {ok,status,data}=await j(api('ask'),{method:'POST',
770
  headers:{'content-type':'application/json'},body:JSON.stringify(body)});
771
+ btn.disabled=false;out.removeAttribute('aria-busy');
772
  if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
773
  +(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
774
  const r=data.receipt||{}, sig=r.signed?'signed':'UNSIGNED';
 
776
  +`<div class="rcpt">receipt: ${sig} · ask ${esc(String(data.ask_id)).slice(0,8)}</div>`;
777
  };
778
  document.getElementById('councilbtn').onclick=async()=>{
779
+ const btn=document.getElementById('councilbtn');
780
  const prompt=document.getElementById('councilprompt').value.trim();
781
  const out=document.getElementById('councilout');
782
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
783
  const picks=[...document.getElementById('councilsel').selectedOptions].map(o=>o.value);
784
  const debate=document.getElementById('debate').checked;
785
  out.textContent=debate?'…convening (debate: 2 bounded rounds)':'…convening';
786
+ btn.disabled=true;out.setAttribute('aria-busy','true');
787
  const body={prompt}; if(picks.length)body.personas=picks; if(debate)body.debate=true;
788
  const {ok,status,data}=await j(api('council'),{method:'POST',
789
  headers:{'content-type':'application/json'},body:JSON.stringify(body)});
790
+ btn.disabled=false;out.removeAttribute('aria-busy');
791
  if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
792
  +(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
793
  const res=data.result||{}, rounds=res.rounds||[], c=data.contract||{};
 
795
  const r1=rounds.filter(t=>(t.round||1)===1), r2=rounds.filter(t=>t.round===2);
796
  const route=c.routing||{}, outer=data.receipt||{};
797
  const contract=`<div class="contract"><b>${esc(c.decision_state||'PROPOSAL_ONLY')}</b>`
798
+ +` · evidence ${esc(c.evidence_state||'UNKNOWN')}`
799
+ +` · human review ${c.human_checkpoint&&c.human_checkpoint.required?'REQUIRED':'UNKNOWN'}`
800
  +`<br>Nemo: ${esc((route.experts_selected||[]).join(' + ')||route.state||'unavailable')}`
801
+ +` · route ${esc(route.state||'UNKNOWN')} · outer receipt ${outer.signed?'SIGNED':'UNSIGNED'}`
802
  +`<br>replay ${esc((c.replay&&c.replay.key)||'unavailable')}`
803
  +`<br>semantic consensus: ${esc((c.semantic_consensus&&c.semantic_consensus.state)||'NOT_MEASURED')}`
804
  +`</div>`;
 
894
  from fastapi import Request
895
  from fastapi.responses import HTMLResponse, JSONResponse
896
 
897
+ council_store, council_storage = _open_council_store(ns)
898
+ try:
899
+ app.state.ayllu_council_khipu = council_store
900
+ app.state.ayllu_council_khipu_storage = council_storage
901
+ except Exception:
902
+ pass
903
+
904
  def _runtime_signer(request: "Request"):
905
  """Resolve the host signer lazily: Ayllu registers before serve.py creates it."""
906
  try:
 
922
  })
923
 
924
  async def _council_manifest(request: "Request") -> "JSONResponse":
925
+ storage = getattr(request.app.state, "ayllu_council_khipu_storage",
926
+ council_storage)
927
+ return JSONResponse(council_manifest(ns, storage))
928
 
929
  async def _ask(request: "Request") -> "JSONResponse":
930
  ok, retry = _ASK_BUCKET.check()
 
960
  return JSONResponse(
961
  {"error": "'difficulty' must be a number between 0 and 1"},
962
  status_code=422)
963
+ async def _ask_complete(**kwargs):
964
+ return await _backend.model_complete(
965
+ **kwargs, max_tokens=ASK_MAX_TOKENS,
966
+ timeout_s=ASK_TURN_TIMEOUT_S)
967
+
968
+ turn = await run_turn(p, prompt, model_complete=_ask_complete,
969
  difficulty=difficulty)
970
  ask_id = str(uuid.uuid4())
971
  receipt = _make_receipt({
 
1027
  return JSONResponse(
1028
  {"error": "no known personas in request",
1029
  "known": [x.name for x in ROSTER]}, status_code=422)
1030
+ async def _council_complete(**kwargs):
1031
+ return await _backend.model_complete(
1032
+ **kwargs, max_tokens=COUNCIL_MAX_TOKENS,
1033
+ timeout_s=COUNCIL_TURN_TIMEOUT_S)
1034
+
1035
+ result = await _LOUNGE.deliberate(
1036
+ prompt, personas, model_complete=_council_complete, debate=debate)
1037
  if cap_note:
1038
  result["cap_note"] = cap_note
1039
  council_id = str(uuid.uuid4())
1040
  signer = _runtime_signer(request)
1041
  nemo_route = _nemo_council_route(prompt, sign_fn=signer)
1042
  contract = _build_council_contract(prompt, result, nemo_route)
1043
+ store = getattr(request.app.state, "ayllu_council_khipu", council_store)
1044
+ storage = getattr(request.app.state, "ayllu_council_khipu_storage",
1045
+ council_storage)
1046
+ contract["chain"] = _mint_council_chain(
1047
+ contract, ns=ns, store=store, storage_meta=storage)
1048
  receipt_body = {
1049
  "council_id": council_id,
1050
  "prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
 
1096
  f"ok — ayllu registered: {len(ROSTER)} personas; live model backend "
1097
  f"({_backend.backend_status().get('mode')}); bounded-autonomy Λ-gate; "
1098
  f"/ayllu + /api/{ns}/v1/ayllu/roster|ask|council|lounge; "
1099
+ f"debate-mode council; council_khipu={council_storage.get('backend')} "
1100
+ f"(process_restart_durable={council_storage.get('durable')}, "
1101
+ f"redeploy=NOT_VERIFIED); version={__version__}"
1102
  )
ayllu/backend.py CHANGED
@@ -13,6 +13,7 @@ honest stub dict — it NEVER fabricates an answer and NEVER claims a wiring it
13
  """
14
  from __future__ import annotations
15
 
 
16
  import contextlib
17
  from typing import Any, Optional
18
 
@@ -39,10 +40,15 @@ def backend_status() -> dict[str, Any]:
39
  orch_err = str(exc)[:160]
40
 
41
  has_cred = False
 
 
42
  cred_checked = False
43
  if orch is not None:
44
  try:
45
- has_cred = bool(orch.has_inference_credential())
 
 
 
46
  cred_checked = True
47
  except Exception:
48
  cred_checked = False
@@ -51,7 +57,7 @@ def backend_status() -> dict[str, Any]:
51
  mode = "unavailable"
52
  elif not cred_checked:
53
  mode = "unknown"
54
- elif has_cred:
55
  mode = "live"
56
  else:
57
  mode = "stub"
@@ -61,15 +67,18 @@ def backend_status() -> dict[str, Any]:
61
  "orchestrator_error": orch_err,
62
  "credential_checked": cred_checked,
63
  "has_credential": has_cred,
 
 
64
  "mode": mode,
65
  "note": {
66
  "unavailable": "a11oy_code_orchestrator not importable — ask/council return "
67
  "an honest stub.",
68
  "unknown": "orchestrator present but credential state could not be read.",
69
- "live": "real model answers via a11oy routing + energy receipts; ongoing "
70
- "token cost.",
71
- "stub": "no inference credential set on this Space — clearly-labeled "
72
- "deterministic stub, no fabrication.",
 
73
  }.get(mode, ""),
74
  "backend": "a11oy_code_orchestrator.agent_model_complete",
75
  }
@@ -83,6 +92,7 @@ async def model_complete(
83
  persona: Optional[str] = None,
84
  max_tokens: int = 1000,
85
  temperature: float = 0.4,
 
86
  **_ignored: Any,
87
  ) -> dict[str, Any]:
88
  """Adapter matching ayllu.loop.run_turn's model_complete contract.
@@ -105,20 +115,57 @@ async def model_complete(
105
  "model": "unavailable",
106
  "stub": True,
107
  }
 
 
108
  try:
109
- result = await _o.agent_model_complete(
110
- messages, max_tokens=max_tokens, temperature=temperature)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  except Exception as exc:
112
  return {
113
- "text": f"[honest error: agent_model_complete raised: {str(exc)[:160]}]",
114
  "model": "error",
115
  "stub": True,
 
 
 
 
 
 
 
116
  }
117
  if not isinstance(result, dict):
118
- return {"text": str(result), "model": "unknown", "stub": True}
 
 
 
 
 
 
 
 
119
  return {
120
  "text": result.get("text", ""),
121
  "model": result.get("model"),
122
  "stub": bool(result.get("stub")),
 
 
 
 
123
  "energy_receipt": result.get("energy_receipt"),
124
  }
 
13
  """
14
  from __future__ import annotations
15
 
16
+ import asyncio
17
  import contextlib
18
  from typing import Any, Optional
19
 
 
40
  orch_err = str(exc)[:160]
41
 
42
  has_cred = False
43
+ local_ready = False
44
+ backend_ready = False
45
  cred_checked = False
46
  if orch is not None:
47
  try:
48
+ has_cred = bool(orch._resolve_hf_token()) or any(
49
+ orch._resolve_provider_keys().values())
50
+ _base, local_ready = orch._serving_base()
51
+ backend_ready = bool(orch.has_inference_credential())
52
  cred_checked = True
53
  except Exception:
54
  cred_checked = False
 
57
  mode = "unavailable"
58
  elif not cred_checked:
59
  mode = "unknown"
60
+ elif backend_ready:
61
  mode = "live"
62
  else:
63
  mode = "stub"
 
67
  "orchestrator_error": orch_err,
68
  "credential_checked": cred_checked,
69
  "has_credential": has_cred,
70
+ "local_backend_ready": local_ready,
71
+ "backend_ready": backend_ready,
72
  "mode": mode,
73
  "note": {
74
  "unavailable": "a11oy_code_orchestrator not importable — ask/council return "
75
  "an honest stub.",
76
  "unknown": "orchestrator present but credential state could not be read.",
77
+ "live": ("real model answers via a11oy routing + receipts; source is a "
78
+ "reachable local backend or a credentialed remote provider. "
79
+ "Outputs remain unverified model text."),
80
+ "stub": ("no reachable local backend or remote inference credential — "
81
+ "clearly-labeled deterministic stub, no fabrication."),
82
  }.get(mode, ""),
83
  "backend": "a11oy_code_orchestrator.agent_model_complete",
84
  }
 
92
  persona: Optional[str] = None,
93
  max_tokens: int = 1000,
94
  temperature: float = 0.4,
95
+ timeout_s: float = 45.0,
96
  **_ignored: Any,
97
  ) -> dict[str, Any]:
98
  """Adapter matching ayllu.loop.run_turn's model_complete contract.
 
115
  "model": "unavailable",
116
  "stub": True,
117
  }
118
+ bounded_tokens = max(1, min(int(max_tokens), 2048))
119
+ bounded_timeout = max(0.1, min(float(timeout_s), 120.0))
120
  try:
121
+ result = await asyncio.wait_for(
122
+ _o.agent_model_complete(
123
+ messages, max_tokens=bounded_tokens, temperature=temperature),
124
+ timeout=bounded_timeout,
125
+ )
126
+ except asyncio.TimeoutError:
127
+ return {
128
+ "text": None,
129
+ "model": "timeout",
130
+ "stub": True,
131
+ "timeout": True,
132
+ "token_budget": bounded_tokens,
133
+ "timeout_s": bounded_timeout,
134
+ "honesty": (
135
+ f"model turn exceeded the {bounded_timeout:g}s deadline; "
136
+ "the request was cancelled and no answer was fabricated"
137
+ ),
138
+ }
139
  except Exception as exc:
140
  return {
141
+ "text": None,
142
  "model": "error",
143
  "stub": True,
144
+ "timeout": False,
145
+ "token_budget": bounded_tokens,
146
+ "timeout_s": bounded_timeout,
147
+ "honesty": (
148
+ f"agent_model_complete raised: {str(exc)[:160]}; "
149
+ "no answer was fabricated"
150
+ ),
151
  }
152
  if not isinstance(result, dict):
153
+ return {
154
+ "text": None,
155
+ "model": "unknown",
156
+ "stub": True,
157
+ "timeout": False,
158
+ "token_budget": bounded_tokens,
159
+ "timeout_s": bounded_timeout,
160
+ "honesty": "model backend returned a non-contract value; no answer was used",
161
+ }
162
  return {
163
  "text": result.get("text", ""),
164
  "model": result.get("model"),
165
  "stub": bool(result.get("stub")),
166
+ "timeout": bool(result.get("timeout", False)),
167
+ "token_budget": bounded_tokens,
168
+ "timeout_s": bounded_timeout,
169
+ "honesty": result.get("honesty"),
170
  "energy_receipt": result.get("energy_receipt"),
171
  }
ayllu/loop.py CHANGED
@@ -70,6 +70,9 @@ async def run_turn(
70
  answer: Optional[str] = None
71
  model: Optional[str] = None
72
  stub: Optional[bool] = None
 
 
 
73
  energy_receipt: Any = None
74
 
75
  if model_complete is None:
@@ -101,11 +104,16 @@ async def run_turn(
101
  answer = result.get("text")
102
  model = result.get("model")
103
  stub = result.get("stub")
 
 
 
104
  energy_receipt = result.get("energy_receipt")
105
  else:
106
  answer = str(result)
107
  honesty = "answer produced by a11oy's model backend" + (
108
  " (clearly-labeled stub — no inference credential set)" if stub else "")
 
 
109
  except Exception as exc:
110
  honesty = (f"model backend raised: {str(exc)[:120]} "
111
  "(honest — no fabricated answer)")
@@ -122,6 +130,9 @@ async def run_turn(
122
  "answer": answer,
123
  "model": model,
124
  "stub": stub,
 
 
 
125
  "energy_receipt": energy_receipt,
126
  "honesty": honesty,
127
  "evidence": [],
 
70
  answer: Optional[str] = None
71
  model: Optional[str] = None
72
  stub: Optional[bool] = None
73
+ timed_out = False
74
+ token_budget: Optional[int] = None
75
+ timeout_s: Optional[float] = None
76
  energy_receipt: Any = None
77
 
78
  if model_complete is None:
 
104
  answer = result.get("text")
105
  model = result.get("model")
106
  stub = result.get("stub")
107
+ timed_out = bool(result.get("timeout", False))
108
+ token_budget = result.get("token_budget")
109
+ timeout_s = result.get("timeout_s")
110
  energy_receipt = result.get("energy_receipt")
111
  else:
112
  answer = str(result)
113
  honesty = "answer produced by a11oy's model backend" + (
114
  " (clearly-labeled stub — no inference credential set)" if stub else "")
115
+ if isinstance(result, dict) and result.get("honesty"):
116
+ honesty = str(result["honesty"])
117
  except Exception as exc:
118
  honesty = (f"model backend raised: {str(exc)[:120]} "
119
  "(honest — no fabricated answer)")
 
130
  "answer": answer,
131
  "model": model,
132
  "stub": stub,
133
+ "timeout": timed_out,
134
+ "token_budget": token_budget,
135
+ "timeout_s": timeout_s,
136
  "energy_receipt": energy_receipt,
137
  "honesty": honesty,
138
  "evidence": [],
ayllu/lounge.py CHANGED
@@ -8,6 +8,7 @@ per persona — and never fabricates when no backend is injected.
8
  """
9
  from __future__ import annotations
10
 
 
11
  import time
12
  from typing import Any
13
 
@@ -39,18 +40,21 @@ class Lounge:
39
  ) -> dict[str, Any]:
40
  from .loop import run_turn
41
 
42
- rounds = []
43
- for p in personas:
44
- turn = await run_turn(
45
- p, prompt,
46
- model_complete=model_complete,
47
  difficulty=difficulty,
48
- two_person_attested=two_person_attested,
49
- )
 
 
 
 
 
 
50
  turn["round"] = 1
51
  src = "brain" if turn.get("answer") is not None else "persona-fallback"
52
  self.post(p.name, turn.get("answer") or turn.get("honesty"), source=src)
53
- rounds.append(turn)
54
 
55
  mode = "single-round"
56
  # Debate-then-converge (after arXiv:2305.14325, Multiagent Debate): one
@@ -65,6 +69,7 @@ class Lounge:
65
  ]
66
  if len(positions) >= 2:
67
  mode = "debate"
 
68
  for p in personas:
69
  peers = "\n\n".join(
70
  f"[{name}] {ans[:1200]}" for name, ans in positions
@@ -77,12 +82,11 @@ class Lounge:
77
  "State explicitly where you agree or dissent, then give "
78
  "your revised answer. Honest dissent beats false consensus."
79
  )
80
- turn = await run_turn(
81
- p, debate_prompt,
82
- model_complete=model_complete,
83
- difficulty=difficulty,
84
- two_person_attested=two_person_attested,
85
- )
86
  turn["round"] = 2
87
  src = ("brain" if turn.get("answer") is not None
88
  else "persona-fallback")
 
8
  """
9
  from __future__ import annotations
10
 
11
+ import asyncio
12
  import time
13
  from typing import Any
14
 
 
40
  ) -> dict[str, Any]:
41
  from .loop import run_turn
42
 
43
+ async def _one_turn(p, turn_prompt: str):
44
+ return await run_turn(
45
+ p, turn_prompt, model_complete=model_complete,
 
 
46
  difficulty=difficulty,
47
+ two_person_attested=two_person_attested)
48
+
49
+ # A council round is independent fan-out. Preserve input order in the
50
+ # evidence, but spend one bounded deadline instead of N sequential ones.
51
+ rounds = list(await asyncio.gather(*(
52
+ _one_turn(p, prompt) for p in personas
53
+ )))
54
+ for p, turn in zip(personas, rounds):
55
  turn["round"] = 1
56
  src = "brain" if turn.get("answer") is not None else "persona-fallback"
57
  self.post(p.name, turn.get("answer") or turn.get("honesty"), source=src)
 
58
 
59
  mode = "single-round"
60
  # Debate-then-converge (after arXiv:2305.14325, Multiagent Debate): one
 
69
  ]
70
  if len(positions) >= 2:
71
  mode = "debate"
72
+ debate_jobs = []
73
  for p in personas:
74
  peers = "\n\n".join(
75
  f"[{name}] {ans[:1200]}" for name, ans in positions
 
82
  "State explicitly where you agree or dissent, then give "
83
  "your revised answer. Honest dissent beats false consensus."
84
  )
85
+ debate_jobs.append((p, debate_prompt))
86
+ revised = list(await asyncio.gather(*(
87
+ _one_turn(p, turn_prompt) for p, turn_prompt in debate_jobs
88
+ )))
89
+ for (p, _turn_prompt), turn in zip(debate_jobs, revised):
 
90
  turn["round"] = 2
91
  src = ("brain" if turn.get("answer") is not None
92
  else "persona-fallback")
research/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Research-layer modules for source-cited A11oy experiments."""
3
+
research/a11oy_primary_project_registry.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # (c) 2026 Lutar, Stephen P. - SZL Holdings - ORCID 0009-0001-0110-4173
4
+ # Change-class: ADDITIVE - research registry only; no route or UI registration.
5
+ """Primary-source project registry with honest live GitHub metadata.
6
+
7
+ This module records projects and organizations, not individual people. The
8
+ static registry is deliberately unranked: inclusion means "study this primary
9
+ source", not "this is objectively first". Every adaptation is DECLARED and
10
+ attributed ``STUDIED_NOT_COPIED``.
11
+
12
+ Live stars, detected SPDX license, and the default-branch revision are fetched
13
+ from GitHub's API only when explicitly requested. They are never embedded in
14
+ the registry. A failed or disabled fetch returns null values carrying the
15
+ ``UNAVAILABLE`` label; it never reuses an expired value as if it were current.
16
+
17
+ Taxonomy home: research/. Pure Python standard library; no HTTP framework.
18
+ """
19
+
20
+ from concurrent.futures import ThreadPoolExecutor, as_completed
21
+ from copy import deepcopy
22
+ from datetime import datetime, timezone
23
+ import json
24
+ import os
25
+ import re
26
+ import threading
27
+ import time
28
+ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
29
+ from urllib.parse import quote, urlparse
30
+ from urllib.request import Request, urlopen
31
+
32
+
33
+ REGISTRY_VERSION = "wave-13-primary-projects-v1"
34
+ ATTRIBUTION = "STUDIED_NOT_COPIED"
35
+ STATIC_LABEL = "DECLARED"
36
+ LIVE_LABEL = "MEASURED"
37
+ UNAVAILABLE = "UNAVAILABLE"
38
+ DEFAULT_TIMEOUT_S = 5.0
39
+ DEFAULT_TTL_S = 15 * 60.0
40
+ MAX_RESPONSE_BYTES = 2_000_000
41
+
42
+
43
+ _FIELDS: Tuple[Mapping[str, str], ...] = (
44
+ {"id": "reasoning_math", "name": "Reasoning & math"},
45
+ {"id": "quantization_efficient_inference", "name": "Quantization & efficient inference"},
46
+ {"id": "retrieval_memory_long_context", "name": "Retrieval, memory & long context"},
47
+ {"id": "multimodal_vision", "name": "Multimodal & vision"},
48
+ {"id": "biomed_science", "name": "Biomed & science"},
49
+ {"id": "security_red_team", "name": "Security & red-team"},
50
+ {"id": "datasets_curation", "name": "Datasets & curation"},
51
+ {"id": "sovereign_on_metal_serving", "name": "Sovereign / on-metal serving"},
52
+ {
53
+ "id": "verifiable_orchestration_provenance",
54
+ "name": "Verifiable orchestration & provenance",
55
+ },
56
+ {"id": "formal_proof_training", "name": "Formal proof & training"},
57
+ )
58
+
59
+
60
+ def _project(
61
+ project_id: str,
62
+ field: str,
63
+ project: str,
64
+ organization: str,
65
+ repo: str,
66
+ license_expected: str,
67
+ primary_paper_docs: Iterable[str],
68
+ adaptation: str,
69
+ ) -> Mapping[str, Any]:
70
+ return {
71
+ "id": project_id,
72
+ "field": field,
73
+ "project": project,
74
+ "organization": organization,
75
+ "canonical_repo_url": repo,
76
+ "license_expected": license_expected,
77
+ "primary_paper_docs": tuple(primary_paper_docs),
78
+ "szl_adaptation_status": STATIC_LABEL,
79
+ "szl_adaptation": adaptation,
80
+ "attribution": ATTRIBUTION,
81
+ }
82
+
83
+
84
+ # Licenses here are expectations to compare against the live API result, not a
85
+ # substitute for inspecting the license at the fetched revision. UNKNOWN is
86
+ # intentional wherever the repository/model license is non-SPDX or uncertain.
87
+ _PROJECTS: Tuple[Mapping[str, Any], ...] = (
88
+ # Reasoning & math
89
+ _project("qwen3", "reasoning_math", "Qwen3", "QwenLM", "https://github.com/QwenLM/Qwen3", "Apache-2.0", ("https://arxiv.org/abs/2505.09388",), "Route bounded math prompts through formula-aware evaluation and attach A11oy provenance."),
90
+ _project("deepseek-r1", "reasoning_math", "DeepSeek-R1", "deepseek-ai", "https://github.com/deepseek-ai/DeepSeek-R1", "MIT", ("https://arxiv.org/abs/2501.12948",), "Evaluate explicit reasoning traces against locked-formula and restraint gates before acceptance."),
91
+ _project("kimi-k2-5", "reasoning_math", "Kimi K2.5", "MoonshotAI", "https://github.com/MoonshotAI/Kimi-K2.5", "UNKNOWN", ("https://github.com/MoonshotAI/Kimi-K2.5/blob/main/tech_report.pdf",), "Study long-horizon decomposition while preserving A11oy budgets, receipts, and human override."),
92
+ _project("glm-4-7", "reasoning_math", "GLM-4.7 (GLM-4.5 repository)", "zai-org", "https://github.com/zai-org/GLM-4.5", "MIT", ("https://github.com/zai-org/GLM-4.5",), "Compare tool-using math runs with the same local formula corpus and honesty labels."),
93
+ _project("minimax-m2-5", "reasoning_math", "MiniMax M2.5", "MiniMax-AI", "https://github.com/MiniMax-AI/MiniMax-M2.5", "UNKNOWN", ("https://github.com/MiniMax-AI/MiniMax-M2.5",), "Test agentic reasoning under explicit step, token, thermal, and energy budgets."),
94
+
95
+ # Quantization & efficient inference
96
+ _project("llama-cpp-inference", "quantization_efficient_inference", "llama.cpp", "ggml-org", "https://github.com/ggml-org/llama.cpp", "MIT", ("https://github.com/ggml-org/llama.cpp/tree/master/examples/quantize",), "Expose local quantization profiles through a hardware-probed A11oy serving plan."),
97
+ _project("vllm-inference", "quantization_efficient_inference", "vLLM", "vllm-project", "https://github.com/vllm-project/vllm", "Apache-2.0", ("https://github.com/vllm-project/vllm/tree/main/docs",), "Adapt paged serving concepts behind A11oy admission control and measured resource receipts."),
98
+ _project("sglang-inference", "quantization_efficient_inference", "SGLang", "sgl-project", "https://github.com/sgl-project/sglang", "Apache-2.0", ("https://github.com/sgl-project/sglang/tree/main/docs",), "Study structured serving and prefix reuse with tenant isolation and bounded caches."),
99
+ _project("bitnet", "quantization_efficient_inference", "BitNet", "microsoft", "https://github.com/microsoft/BitNet", "MIT", ("https://arxiv.org/abs/2402.17764",), "Benchmark low-bit kernels on the actual laptop before declaring any supported profile."),
100
+ _project("tensorrt-llm", "quantization_efficient_inference", "TensorRT-LLM", "NVIDIA", "https://github.com/NVIDIA/TensorRT-LLM", "Apache-2.0", ("https://github.com/NVIDIA/TensorRT-LLM/tree/main/docs",), "Compare engine plans with reproducible revisions, VRAM evidence, and measured throughput."),
101
+
102
+ # Retrieval, memory & long context
103
+ _project("cognee", "retrieval_memory_long_context", "cognee", "topoteretes", "https://github.com/topoteretes/cognee", "Apache-2.0", ("https://github.com/topoteretes/cognee/tree/main/docs",), "Study graph-backed ingestion while retaining A11oy source digests and deletion controls."),
104
+ _project("letta", "retrieval_memory_long_context", "Letta", "letta-ai", "https://github.com/letta-ai/letta", "Apache-2.0", ("https://github.com/letta-ai/letta/tree/main/docs",), "Adapt bounded agent memory with explicit provenance, retention, and operator-visible state."),
105
+ _project("mem0", "retrieval_memory_long_context", "Mem0", "mem0ai", "https://github.com/mem0ai/mem0", "Apache-2.0", ("https://github.com/mem0ai/mem0/tree/main/docs",), "Evaluate memory extraction behind consent, namespace isolation, and auditable forgetting."),
106
+ _project("graphiti", "retrieval_memory_long_context", "Graphiti", "getzep", "https://github.com/getzep/graphiti", "Apache-2.0", ("https://github.com/getzep/graphiti",), "Study temporal knowledge graphs with source-time and ingestion-time retained separately."),
107
+ _project("zep-ce", "retrieval_memory_long_context", "Zep Community Edition", "getzep", "https://github.com/getzep/zep", "Apache-2.0", ("https://github.com/getzep/zep",), "Compare self-hosted memory semantics without claiming parity or importing implementation code."),
108
+
109
+ # Multimodal & vision
110
+ _project("qwen3-vl", "multimodal_vision", "Qwen3-VL", "QwenLM", "https://github.com/QwenLM/Qwen3-VL", "Apache-2.0", ("https://github.com/QwenLM/Qwen3-VL",), "Gate image and document observations as cited evidence, never as unqualified ground truth."),
111
+ _project("glm-v", "multimodal_vision", "GLM-V / GLM-4.5V", "zai-org", "https://github.com/zai-org/GLM-V", "MIT", ("https://arxiv.org/abs/2507.01006",), "Study multimodal reasoning with modality-specific confidence and redaction before retention."),
112
+ _project("internvl", "multimodal_vision", "InternVL", "OpenGVLab", "https://github.com/OpenGVLab/InternVL", "MIT", ("https://arxiv.org/abs/2312.14238",), "Evaluate open multimodal checkpoints through a reproducible visual task harness."),
113
+ _project("janus", "multimodal_vision", "Janus", "deepseek-ai", "https://github.com/deepseek-ai/Janus", "MIT", ("https://arxiv.org/abs/2410.13848",), "Separate understanding and generation evidence paths in A11oy receipts."),
114
+ _project("minicpm-o", "multimodal_vision", "MiniCPM-o", "OpenBMB", "https://github.com/OpenBMB/MiniCPM-o", "UNKNOWN", ("https://github.com/OpenBMB/MiniCPM-o",), "Probe laptop-feasible multimodal inference with measured latency and explicit modality limits."),
115
+
116
+ # Biomed & science
117
+ _project("gpt-oss", "biomed_science", "gpt-oss", "openai", "https://github.com/openai/gpt-oss", "Apache-2.0", ("https://github.com/openai/gpt-oss",), "Evaluate scientific reasoning only on cited corpora with domain-expert review required."),
118
+ _project("glm-4-5v-science", "biomed_science", "GLM-4.5V", "zai-org", "https://github.com/zai-org/GLM-V", "MIT", ("https://arxiv.org/abs/2507.01006",), "Test chart and document understanding without upgrading it to clinical validity."),
119
+ _project("deepseek-r1-science", "biomed_science", "DeepSeek-R1", "deepseek-ai", "https://github.com/deepseek-ai/DeepSeek-R1", "MIT", ("https://arxiv.org/abs/2501.12948",), "Run scientific derivations through unit, citation, and formal-invariant checks."),
120
+ _project("openmed", "biomed_science", "OpenMed", "maziyarpanahi", "https://github.com/maziyarpanahi/openmed", "Apache-2.0", ("https://arxiv.org/abs/2508.01630",), "Study local clinical NLP with privacy boundaries; outputs remain non-diagnostic and review-gated."),
121
+ _project("physicsnemo", "biomed_science", "PhysicsNeMo", "NVIDIA", "https://github.com/NVIDIA/physicsnemo", "Apache-2.0", ("https://github.com/NVIDIA/physicsnemo/tree/main/docs",), "Map physics residuals into the existing A11oy formula and evidence gates."),
122
+
123
+ # Security & red-team
124
+ _project("garak", "security_red_team", "garak", "NVIDIA", "https://github.com/NVIDIA/garak", "Apache-2.0", ("https://github.com/NVIDIA/garak/tree/main/docs",), "Translate probe outcomes into deny-by-default test evidence, not a blanket safety claim."),
125
+ _project("pyrit", "security_red_team", "PyRIT", "Azure", "https://github.com/Azure/PyRIT", "MIT", ("https://github.com/Azure/PyRIT/tree/main/doc",), "Adapt orchestrated red-team cases to governed, rate-limited A11oy evaluation runs."),
126
+ _project("owasp-genai-top10", "security_red_team", "OWASP Top 10 for LLM Applications", "OWASP", "https://github.com/OWASP/www-project-top-10-for-large-language-model-applications", "CC-BY-SA-4.0", ("https://genai.owasp.org/llm-top-10/",), "Crosswalk each risk category to enforceable gates and evidence-bearing tests."),
127
+ _project("promptfoo", "security_red_team", "promptfoo", "promptfoo", "https://github.com/promptfoo/promptfoo", "MIT", ("https://github.com/promptfoo/promptfoo/tree/main/site/docs",), "Study declarative adversarial evaluations while keeping A11oy policy decisions local."),
128
+ _project("llm-guard", "security_red_team", "LLM Guard", "ProtectAI", "https://github.com/protectai/llm-guard", "MIT", ("https://github.com/protectai/llm-guard/tree/main/docs",), "Compare input/output scanners as advisory signals under the constitutional gate."),
129
+
130
+ # Datasets & curation
131
+ _project("hf-datasets", "datasets_curation", "Datasets", "huggingface", "https://github.com/huggingface/datasets", "Apache-2.0", ("https://github.com/huggingface/datasets/tree/main/docs",), "Record dataset revisions, configuration, splits, and source licenses in ingestion receipts."),
132
+ _project("kagglehub", "datasets_curation", "KaggleHub", "Kaggle", "https://github.com/Kaggle/kagglehub", "Apache-2.0", ("https://github.com/Kaggle/kagglehub",), "Resolve assets into a quarantined cache with checksums and explicit terms review."),
133
+ _project("openml-python", "datasets_curation", "OpenML Python", "openml", "https://github.com/openml/openml-python", "BSD-3-Clause", ("https://github.com/openml/openml-python/tree/main/doc",), "Preserve task and dataset identifiers so experiments can be replayed exactly."),
134
+ _project("croissant", "datasets_curation", "Croissant", "mlcommons", "https://github.com/mlcommons/croissant", "Apache-2.0", ("https://docs.mlcommons.org/croissant/docs/croissant-spec.html",), "Emit Croissant-compatible metadata alongside A11oy provenance without replacing receipts."),
135
+ _project("datatrove", "datasets_curation", "DataTrove", "huggingface", "https://github.com/huggingface/datatrove", "Apache-2.0", ("https://github.com/huggingface/datatrove/tree/main/docs",), "Study scalable filtering with auditable rejection reasons and reversible curation manifests."),
136
+
137
+ # Sovereign / on-metal serving
138
+ _project("vllm-serving", "sovereign_on_metal_serving", "vLLM", "vllm-project", "https://github.com/vllm-project/vllm", "Apache-2.0", ("https://github.com/vllm-project/vllm/tree/main/docs",), "Run only profiles admitted by real VRAM, driver, and model-license probes."),
139
+ _project("ollama", "sovereign_on_metal_serving", "Ollama", "ollama", "https://github.com/ollama/ollama", "MIT", ("https://github.com/ollama/ollama/tree/main/docs",), "Use a loopback-only local backend with explicit model digests and bounded concurrency."),
140
+ _project("llama-cpp-serving", "sovereign_on_metal_serving", "llama.cpp", "ggml-org", "https://github.com/ggml-org/llama.cpp", "MIT", ("https://github.com/ggml-org/llama.cpp/tree/master/examples/server",), "Adapt the local server behind A11oy authentication, quotas, and receipt-on-write rules."),
141
+ _project("sglang-serving", "sovereign_on_metal_serving", "SGLang", "sgl-project", "https://github.com/sgl-project/sglang", "Apache-2.0", ("https://github.com/sgl-project/sglang/tree/main/docs",), "Study high-throughput local serving with isolation and honest capacity reporting."),
142
+ _project("kserve", "sovereign_on_metal_serving", "KServe", "kserve", "https://github.com/kserve/kserve", "Apache-2.0", ("https://github.com/kserve/website/tree/main/docs",), "Map portable serving declarations to signed deployment policy and rollback evidence."),
143
+
144
+ # Verifiable orchestration & provenance
145
+ _project("risc0", "verifiable_orchestration_provenance", "RISC Zero zkVM", "risc0", "https://github.com/risc0/risc0", "Apache-2.0 OR MIT", ("https://dev.risczero.com/proof-system-in-detail.pdf",), "Explore bounded proof adapters while labeling unproved A11oy paths as unavailable."),
146
+ _project("rekor", "verifiable_orchestration_provenance", "Rekor", "sigstore", "https://github.com/sigstore/rekor", "Apache-2.0", ("https://github.com/sigstore/rekor/tree/main/docs",), "Anchor selected receipt digests to transparency evidence without signing on reads."),
147
+ _project("in-toto", "verifiable_orchestration_provenance", "in-toto", "in-toto", "https://github.com/in-toto/in-toto", "Apache-2.0", ("https://github.com/in-toto/docs",), "Map A11oy action receipts to supply-chain step attestations with verified identities."),
148
+ _project("slsa", "verifiable_orchestration_provenance", "SLSA", "slsa-framework", "https://github.com/slsa-framework/slsa", "Apache-2.0", ("https://slsa.dev/spec/v1.2/",), "Use SLSA levels as externally defined criteria, never as a self-awarded badge."),
149
+ _project("ezkl", "verifiable_orchestration_provenance", "EZKL", "zkonduit", "https://github.com/zkonduit/ezkl", "MIT", ("https://github.com/zkonduit/ezkl/tree/main/docs",), "Prototype proof-carrying small-model inference and report unsupported operators honestly."),
150
+ _project("opengradient", "verifiable_orchestration_provenance", "OpenGradient SDK", "OpenGradient", "https://github.com/OpenGradient/sdk", "UNKNOWN", ("https://github.com/OpenGradient/sdk/tree/main/docs",), "Study externally verifiable execution receipts without treating third-party claims as local proof."),
151
+
152
+ # Formal proof & training
153
+ _project("lean4", "formal_proof_training", "Lean 4", "leanprover", "https://github.com/leanprover/lean4", "Apache-2.0", ("https://github.com/leanprover/lean4/tree/master/doc",), "Keep runtime formula claims linked to checked theorem names and exact proof revisions."),
154
+ _project("mathlib4", "formal_proof_training", "mathlib4", "leanprover-community", "https://github.com/leanprover-community/mathlib4", "Apache-2.0", ("https://github.com/leanprover-community/mathlib4/tree/master/Mathlib",), "Study reusable lemmas while maintaining A11oy theorem ownership and dependency manifests."),
155
+ _project("trl", "formal_proof_training", "TRL", "huggingface", "https://github.com/huggingface/trl", "Apache-2.0", ("https://github.com/huggingface/trl/tree/main/docs",), "Run post-training experiments as versioned recipes with baseline, seed, and evaluation receipts."),
156
+ _project("peft", "formal_proof_training", "PEFT", "huggingface", "https://github.com/huggingface/peft", "Apache-2.0", ("https://github.com/huggingface/peft/tree/main/docs",), "Prefer laptop-feasible adapters and record base-model plus adapter revisions independently."),
157
+ _project("unsloth", "formal_proof_training", "Unsloth", "unslothai", "https://github.com/unslothai/unsloth", "Apache-2.0", ("https://github.com/unslothai/unsloth/tree/main/docs",), "Study memory-efficient fine-tuning only after a hardware probe selects a safe recipe."),
158
+ )
159
+
160
+
161
+ _REPO_PATH = re.compile(r"^/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/?$")
162
+ _CACHE: Dict[str, Tuple[float, Mapping[str, Any]]] = {}
163
+ _CACHE_LOCK = threading.RLock()
164
+
165
+
166
+ def _repo_slug(repo_url: str) -> str:
167
+ parsed = urlparse(repo_url)
168
+ if parsed.scheme != "https" or parsed.netloc.lower() != "github.com":
169
+ raise ValueError("canonical_repo_url must be an https://github.com owner/repo URL")
170
+ if parsed.query or parsed.fragment or not _REPO_PATH.fullmatch(parsed.path):
171
+ raise ValueError("canonical_repo_url must not contain a subpath, query, or fragment")
172
+ owner, repo = parsed.path.strip("/").split("/", 1)
173
+ if repo.lower().endswith(".git"):
174
+ repo = repo[:-4]
175
+ return f"{owner}/{repo}"
176
+
177
+
178
+ def _iso_utc(epoch_s: float) -> str:
179
+ return datetime.fromtimestamp(epoch_s, timezone.utc).isoformat().replace("+00:00", "Z")
180
+
181
+
182
+ def _safe_reason(exc: BaseException) -> str:
183
+ text = " ".join(str(exc).split()) or exc.__class__.__name__
184
+ return text[:240]
185
+
186
+
187
+ def _unavailable(repo_url: str, reason: str) -> Dict[str, Any]:
188
+ return {
189
+ "label": UNAVAILABLE,
190
+ "freshness": UNAVAILABLE,
191
+ "source": "GitHub REST API",
192
+ "source_url": f"https://api.github.com/repos/{_repo_slug(repo_url)}",
193
+ "stars": None,
194
+ "stars_label": UNAVAILABLE,
195
+ "license": None,
196
+ "license_label": UNAVAILABLE,
197
+ "revision": None,
198
+ "revision_label": UNAVAILABLE,
199
+ "default_branch": None,
200
+ "fetched_at": None,
201
+ "fetched_at_label": UNAVAILABLE,
202
+ "reason": reason,
203
+ }
204
+
205
+
206
+ def _request_headers() -> Dict[str, str]:
207
+ headers = {
208
+ "Accept": "application/vnd.github+json",
209
+ "X-GitHub-Api-Version": "2022-11-28",
210
+ "User-Agent": "a11oy-primary-project-registry/1.0",
211
+ }
212
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
213
+ if token:
214
+ headers["Authorization"] = f"Bearer {token}"
215
+ return headers
216
+
217
+
218
+ def _read_json(url: str, timeout_s: float, opener: Callable[..., Any]) -> Mapping[str, Any]:
219
+ request = Request(url, headers=_request_headers(), method="GET")
220
+ response = opener(request, timeout=timeout_s)
221
+ try:
222
+ raw = response.read(MAX_RESPONSE_BYTES + 1)
223
+ finally:
224
+ close = getattr(response, "close", None)
225
+ if callable(close):
226
+ close()
227
+ if len(raw) > MAX_RESPONSE_BYTES:
228
+ raise ValueError("GitHub response exceeded the bounded response size")
229
+ payload = json.loads(raw.decode("utf-8"))
230
+ if not isinstance(payload, dict):
231
+ raise ValueError("GitHub response was not a JSON object")
232
+ return payload
233
+
234
+
235
+ def clear_cache() -> None:
236
+ """Clear only the in-memory live-metadata cache (primarily for tests)."""
237
+ with _CACHE_LOCK:
238
+ _CACHE.clear()
239
+
240
+
241
+ def fetch_github_metadata(
242
+ repo_url: str,
243
+ *,
244
+ timeout_s: float = DEFAULT_TIMEOUT_S,
245
+ ttl_s: float = DEFAULT_TTL_S,
246
+ opener: Optional[Callable[..., Any]] = None,
247
+ now: Optional[float] = None,
248
+ ) -> Dict[str, Any]:
249
+ """Fetch stars, SPDX license, and the default-branch commit revision.
250
+
251
+ ``opener`` is injectable for deterministic tests and must follow the small
252
+ ``urllib.request.urlopen(request, timeout=...)`` interface. A cache entry is
253
+ used only while it is inside ``ttl_s``. Any fetch/parse/shape failure returns
254
+ an ``UNAVAILABLE`` envelope with null live values.
255
+ """
256
+ slug = _repo_slug(repo_url)
257
+ if not isinstance(timeout_s, (int, float)) or not 0 < float(timeout_s) <= 30.0:
258
+ raise ValueError("timeout_s must be in (0, 30]")
259
+ if not isinstance(ttl_s, (int, float)) or float(ttl_s) < 0:
260
+ raise ValueError("ttl_s must be non-negative")
261
+ current = time.time() if now is None else float(now)
262
+
263
+ with _CACHE_LOCK:
264
+ cached = _CACHE.get(slug)
265
+ if cached and current - cached[0] < float(ttl_s):
266
+ result = deepcopy(cached[1])
267
+ result["freshness"] = "CACHE_FRESH"
268
+ result["cache_age_s"] = round(max(0.0, current - cached[0]), 3)
269
+ return result
270
+
271
+ open_url = urlopen if opener is None else opener
272
+ repo_api = f"https://api.github.com/repos/{slug}"
273
+ try:
274
+ repo_payload = _read_json(repo_api, float(timeout_s), open_url)
275
+ stars = repo_payload.get("stargazers_count")
276
+ branch = repo_payload.get("default_branch")
277
+ if isinstance(stars, bool) or not isinstance(stars, int) or stars < 0:
278
+ raise ValueError("GitHub stargazers_count was missing or invalid")
279
+ if not isinstance(branch, str) or not branch.strip():
280
+ raise ValueError("GitHub default_branch was missing or invalid")
281
+
282
+ commit_api = f"{repo_api}/commits/{quote(branch, safe='')}"
283
+ commit_payload = _read_json(commit_api, float(timeout_s), open_url)
284
+ revision = commit_payload.get("sha")
285
+ if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", revision):
286
+ raise ValueError("GitHub commit sha was missing or invalid")
287
+
288
+ license_obj = repo_payload.get("license")
289
+ spdx = license_obj.get("spdx_id") if isinstance(license_obj, dict) else None
290
+ if not isinstance(spdx, str) or not spdx or spdx == "NOASSERTION":
291
+ spdx = None
292
+ result: Dict[str, Any] = {
293
+ "label": LIVE_LABEL,
294
+ "freshness": "LIVE",
295
+ "source": "GitHub REST API",
296
+ "source_url": repo_api,
297
+ "stars": stars,
298
+ "stars_label": LIVE_LABEL,
299
+ "license": spdx,
300
+ "license_label": LIVE_LABEL if spdx else UNAVAILABLE,
301
+ "revision": revision.lower(),
302
+ "revision_label": LIVE_LABEL,
303
+ "default_branch": branch,
304
+ "fetched_at": _iso_utc(current),
305
+ "fetched_at_label": LIVE_LABEL,
306
+ "reason": None,
307
+ }
308
+ except Exception as exc: # urllib, timeout, decoding, and schema failures
309
+ return _unavailable(repo_url, f"live GitHub metadata unavailable: {_safe_reason(exc)}")
310
+
311
+ with _CACHE_LOCK:
312
+ _CACHE[slug] = (current, deepcopy(result))
313
+ return deepcopy(result)
314
+
315
+
316
+ def projects() -> List[Dict[str, Any]]:
317
+ """Return a caller-owned copy of the unranked static registry."""
318
+ return deepcopy(list(_PROJECTS))
319
+
320
+
321
+ def info() -> Dict[str, Any]:
322
+ """Return a deterministic, JSON-ready description of this registry."""
323
+ counts = {field["id"]: 0 for field in _FIELDS}
324
+ for item in _PROJECTS:
325
+ counts[item["field"]] += 1
326
+ fields = [
327
+ {"id": field["id"], "name": field["name"], "project_count": counts[field["id"]]}
328
+ for field in _FIELDS
329
+ ]
330
+ return {
331
+ "ok": True,
332
+ "service": "a11oy.primary_project_registry",
333
+ "version": REGISTRY_VERSION,
334
+ "label": STATIC_LABEL,
335
+ "attribution": ATTRIBUTION,
336
+ "ranking": "NONE",
337
+ "scope": "projects_and_organizations_only",
338
+ "source_policy": "primary official GitHub, standards, and paper URLs only",
339
+ "license_policy": "license_expected is static guidance; live license is reported beside a fetched revision",
340
+ "live_metadata_policy": "never hand-typed; explicit fetch, bounded timeout, TTL cache, UNAVAILABLE on failure",
341
+ "field_count": len(_FIELDS),
342
+ "project_count": len(_PROJECTS),
343
+ "fields": fields,
344
+ }
345
+
346
+
347
+ def _field_selection(fields: Optional[Iterable[str]]) -> Tuple[str, ...]:
348
+ allowed = tuple(field["id"] for field in _FIELDS)
349
+ if fields is None:
350
+ return allowed
351
+ requested = tuple(dict.fromkeys(fields))
352
+ unknown = sorted(set(requested) - set(allowed))
353
+ if unknown:
354
+ raise ValueError(f"unknown field id(s): {', '.join(unknown)}")
355
+ return requested
356
+
357
+
358
+ def snapshot(
359
+ *,
360
+ fetch_live: bool = False,
361
+ fields: Optional[Iterable[str]] = None,
362
+ timeout_s: float = DEFAULT_TIMEOUT_S,
363
+ ttl_s: float = DEFAULT_TTL_S,
364
+ max_workers: int = 5,
365
+ opener: Optional[Callable[..., Any]] = None,
366
+ now: Optional[float] = None,
367
+ ) -> Dict[str, Any]:
368
+ """Return a JSON-ready registry snapshot; performs no network I/O by default.
369
+
370
+ With ``fetch_live=True``, unique repositories are fetched concurrently and
371
+ results are mapped back to every field entry. Duplicate cross-field projects
372
+ therefore share one revision-bound observation. This function registers no
373
+ HTTP route; a caller may expose the returned payload separately.
374
+ """
375
+ selected = set(_field_selection(fields))
376
+ items = [deepcopy(dict(item)) for item in _PROJECTS if item["field"] in selected]
377
+
378
+ if not fetch_live:
379
+ for item in items:
380
+ item["live_metadata"] = _unavailable(
381
+ item["canonical_repo_url"],
382
+ "live fetch disabled; static registry only",
383
+ )
384
+ payload = info()
385
+ payload.update({
386
+ "live_metadata_requested": False,
387
+ "live_metadata_summary": {LIVE_LABEL: 0, UNAVAILABLE: len(items)},
388
+ "selected_fields": [field["id"] for field in _FIELDS if field["id"] in selected],
389
+ "snapshot_project_count": len(items),
390
+ "unique_repository_count": len({item["canonical_repo_url"] for item in items}),
391
+ "projects": items,
392
+ })
393
+ return payload
394
+
395
+ if not isinstance(max_workers, int) or not 1 <= max_workers <= 16:
396
+ raise ValueError("max_workers must be in [1, 16]")
397
+ unique_repos = tuple(dict.fromkeys(item["canonical_repo_url"] for item in items))
398
+ metadata: Dict[str, Mapping[str, Any]] = {}
399
+
400
+ def fetch(repo_url: str) -> Mapping[str, Any]:
401
+ return fetch_github_metadata(
402
+ repo_url,
403
+ timeout_s=timeout_s,
404
+ ttl_s=ttl_s,
405
+ opener=opener,
406
+ now=now,
407
+ )
408
+
409
+ with ThreadPoolExecutor(max_workers=min(max_workers, max(1, len(unique_repos)))) as pool:
410
+ futures = {pool.submit(fetch, repo): repo for repo in unique_repos}
411
+ for future in as_completed(futures):
412
+ repo = futures[future]
413
+ try:
414
+ metadata[repo] = future.result()
415
+ except Exception as exc: # defensive: one project must not abort the snapshot
416
+ metadata[repo] = _unavailable(repo, f"live metadata worker unavailable: {_safe_reason(exc)}")
417
+
418
+ counts = {LIVE_LABEL: 0, UNAVAILABLE: 0}
419
+ for item in items:
420
+ observed = deepcopy(metadata[item["canonical_repo_url"]])
421
+ item["live_metadata"] = observed
422
+ counts[observed["label"] if observed["label"] == LIVE_LABEL else UNAVAILABLE] += 1
423
+ payload = info()
424
+ payload.update({
425
+ "live_metadata_requested": True,
426
+ "live_metadata_summary": counts,
427
+ "selected_fields": [field["id"] for field in _FIELDS if field["id"] in selected],
428
+ "snapshot_project_count": len(items),
429
+ "unique_repository_count": len(unique_repos),
430
+ "projects": items,
431
+ })
432
+ return payload
433
+
434
+
435
+ __all__ = [
436
+ "ATTRIBUTION",
437
+ "DEFAULT_TIMEOUT_S",
438
+ "DEFAULT_TTL_S",
439
+ "LIVE_LABEL",
440
+ "REGISTRY_VERSION",
441
+ "STATIC_LABEL",
442
+ "UNAVAILABLE",
443
+ "clear_cache",
444
+ "fetch_github_metadata",
445
+ "info",
446
+ "projects",
447
+ "snapshot",
448
+ ]
serve.py CHANGED
@@ -101,7 +101,119 @@ except ImportError:
101
  # --- end OTel preamble ---
102
 
103
 
104
- app = FastAPI(title="a11oy Brand Orchestration Layer", version="2.0.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  # ── Shared guarded-surface wrapper (Wave R Dev 2) — one bad surface must NEVER 500 the
107
  # SPA. This ASGI middleware converts an unhandled exception from any JSON API surface
@@ -216,19 +328,20 @@ except Exception as _szl_bh_e: # pragma: no cover
216
  # fail-open (tracing NEVER breaks a request); locked=8; Λ=Conjecture 1; no key.
217
  try:
218
  import szl_observability as _szl_observability
219
- _szl_observability.register(app, ns="a11oy")
220
- # -- ADDITIVE (free-first spend guardrail) -- szl_spend_cap
221
- try:
222
- import szl_spend_cap as _szl_spend_cap
223
- print("[a11oy] " + _szl_spend_cap.register(app, ns="a11oy"), file=__import__("sys").stderr)
224
- except Exception as _szl_spend_cap_e:
225
- print(f"[szl_spend_cap] NOT mounted ({_szl_spend_cap_e!r}); existing routes unaffected", file=__import__("sys").stderr)
226
- # -- ADDITIVE (free-first spend guardrail) -- szl_spend_cap end
227
- print("[a11oy] Observability registered: /api/a11oy/v1/observability/{traces,trace/{id},health-summary} (OpenTelemetry-style tracing, stdlib)", file=__import__("sys").stderr)
228
  except Exception as _szl_obs_e: # pragma: no cover
229
- print(f"[a11oy] Observability NOT registered: {_szl_obs_e!r}; existing routes unaffected", file=__import__("sys").stderr)
230
  # ── Observability / distributed tracing (devN) — szl_observability ── end
231
 
 
 
 
 
 
 
 
 
232
  # ── Resilience (devM) — szl_resilience ──
233
  # ADDITIVE on top of backend-hardening (#346) + prod-hardening (#345). Two proven
234
  # patterns, our own implementation: (1) a Hystrix-style CIRCUIT BREAKER that wraps
@@ -2886,8 +2999,10 @@ try:
2886
  print(f"[a11oy] /about/thesis registered: {_thesis_status}", file=_sys_th.stderr)
2887
  except Exception as _th_e:
2888
  import sys as _sys_th, traceback as _tb_th
2889
- print(f"[a11oy] /about/thesis NOT registered: {_th_e}", file=_sys_th.stderr)
2890
- _tb_th.print_exc()
 
 
2891
  # ── end /about/thesis ────────────────────────────────────────────────────────
2892
 
2893
  # ── Provenance Hardening (Yachay / Doctrine v12) — ADDITIVE, registered EARLY ──
@@ -3309,7 +3424,9 @@ try:
3309
  app.include_router(_formulas_mod.router)
3310
  print("[a11oy] SZL_FORMULA_OPS mounted: 8x POST /formulas/<name> + /formulas-ops (UI) + /api/a11oy/v1/formulas-ops (index)", file=sys.stderr)
3311
  except Exception as _formulas_exc: # additive: never break the Space if the module is absent
3312
- print(f"[a11oy] FORMULAS mount skipped: {_formulas_exc}", file=sys.stderr)
 
 
3313
 
3314
  # ---------------------------------------------------------------------------
3315
  # ADDITIVE (Yachay / Doctrine v12 PURIQ): mount the a11oy.code conversational
@@ -3870,8 +3987,10 @@ try:
3870
  print(f"[a11oy] v4 PAC-Bayes Predict registered: {_v4_predict_status}", file=_v4p_sys.stderr)
3871
  except Exception as _v4p_e:
3872
  import sys as _v4p_sys, traceback as _v4p_tb
3873
- print(f"[a11oy] v4 PAC-Bayes Predict NOT registered: {_v4p_e!r}", file=_v4p_sys.stderr)
3874
- _v4p_tb.print_exc()
 
 
3875
 
3876
  try:
3877
  import a11oy_v4_thesis_primitives as _v4_tp
@@ -3880,8 +3999,11 @@ try:
3880
  print(f"[a11oy] v4 Thesis Primitives registered: {_v4_tp_status}", file=_v4tp_sys.stderr)
3881
  except Exception as _v4tp_e:
3882
  import sys as _v4tp_sys, traceback as _v4tp_tb
3883
- print(f"[a11oy] v4 Thesis Primitives NOT registered: {_v4tp_e!r}", file=_v4tp_sys.stderr)
3884
- _v4tp_tb.print_exc()
 
 
 
3885
  # --- end PAC-Bayes Predict + Thesis Primitives ---
3886
 
3887
 
@@ -4918,29 +5040,11 @@ except Exception as _sc_e: # pragma: no cover
4918
  print(f"[a11oy] szl_sovereign_compute NOT registered ({_sc_e!r}); existing routes unaffected", file=sys.stderr)
4919
 
4920
 
4921
- # ===========================================================================
4922
- # a11oy OBSERVABILITY (ADDITIVE, 2026-06-01, Yachay / Perplexity Computer Agent).
4923
- # Business observability instilled as a NATIVE a11oy capability NOT a separate
4924
- # product, NOT an add-on, NOT a separate brand. a11oy is the platform; observability
4925
- # is one of its endpoints. szl_observability.register(app, ns="a11oy") adds, under
4926
- # /api/a11oy/v3/observability/* : manifesto, pillars (9), pillars/{name}, tag,
4927
- # attribute-revenue, query (Honeycomb-style), compliance/{framework}, decision-replay,
4928
- # and a mobile-first /dashboard. Reads the REAL in-process organs (Wire D Khipu DAG
4929
- # via app.state.szl_emit_signed_receipt / szl_khipu_dag, trace state) — a pillar with
4930
- # no wired source honestly reports status="unknown" (never faked). Registered BEFORE
4931
- # the /api/a11oy/{path:path} Node proxy + SPA catch-all so /api/a11oy/v3/observability/*
4932
- # resolve LOCALLY. try/except-guarded: can NEVER take down a route.
4933
- # DIFFERENTIATOR: the only observability stack that signs every event (Wire D DSSE),
4934
- # proves the chain via Lean (749/14/163), and replays decisions years later (AYNI-OS).
4935
- # LOCKED preserved: Doctrine v11 749/14/163, 13-axis, SLSA L1, Λ-uniqueness=Conjecture 1.
4936
- # ---------------------------------------------------------------------------
4937
- try:
4938
- import szl_observability as _obs
4939
- _obs_info = _obs.register(app, ns="a11oy")
4940
- print(f"[szl_observability] a11oy observability mounted: base={_obs_info.get('base')}, "
4941
- f"pillars={_obs_info.get('pillars')}, slsa={_obs_info.get('slsa')}", file=sys.stderr)
4942
- except Exception as _obs_e: # pragma: no cover - defensive, additive-only
4943
- print(f"[szl_observability] a11oy observability NOT mounted ({_obs_e!r}); existing routes unaffected", file=sys.stderr)
4944
 
4945
 
4946
  # ===========================================================================
@@ -5129,8 +5233,9 @@ try:
5129
  print("[a11oy] szl_kernels_organ: 9 living kernels at /api/a11oy/v3/kernels/*", file=sys.stderr)
5130
  except Exception as _ke:
5131
  import traceback as _tb_k
5132
- print(f"[a11oy] szl_kernels_organ NOT registered: {_ke}", file=sys.stderr)
5133
- _tb_k.print_exc()
 
5134
 
5135
 
5136
  # ---------------------------------------------------------------------------
@@ -5225,8 +5330,9 @@ try:
5225
  print(f"[a11oy] Typed Ontology + Object Explorer registered: {_ont_status}", file=sys.stderr)
5226
  except Exception as _ont_e:
5227
  import traceback as _ont_tb
5228
- print(f"[a11oy] Typed Ontology NOT registered: {_ont_e!r}", file=sys.stderr)
5229
- _ont_tb.print_exc(file=sys.stderr)
 
5230
 
5231
  # C. Derivation DAG renderer (/api/a11oy/v4/derivation/{id}, /derivation/{id}, vendored Three.js)
5232
  try:
@@ -5235,8 +5341,9 @@ try:
5235
  print(f"[a11oy] Derivation DAG renderer registered: {_deriv_status}", file=sys.stderr)
5236
  except Exception as _deriv_e:
5237
  import traceback as _deriv_tb
5238
- print(f"[a11oy] Derivation DAG NOT registered: {_deriv_e!r}", file=sys.stderr)
5239
- _deriv_tb.print_exc(file=sys.stderr)
 
5240
 
5241
  # B. Synchronized 4-lens shell (/explorer)
5242
  try:
@@ -5245,8 +5352,9 @@ try:
5245
  print(f"[a11oy] 4-lens synchronized Explorer registered: {_explorer_status}", file=sys.stderr)
5246
  except Exception as _explorer_e:
5247
  import traceback as _explorer_tb
5248
- print(f"[a11oy] 4-lens Explorer NOT registered: {_explorer_e!r}", file=sys.stderr)
5249
- _explorer_tb.print_exc(file=sys.stderr)
 
5250
 
5251
  # Every /agent/ask and /predict call writes the full Worker->Critic->Yuyay-13->Lambda->
5252
  # Khipu derivation chain into the Khipu (Receipt) store as a graph (Palantir AIP Logic
@@ -10062,7 +10170,13 @@ async def api_proxy(request: Request, path: str) -> Response:
10062
  # ===========================================================================
10063
  try:
10064
  from fastapi.responses import Response as _VendResponse
 
 
 
 
10065
  _VENDOR_DIR = Path("/app/static-vendor")
 
 
10066
  _VENDOR_JS_CT = "application/javascript; charset=utf-8"
10067
  _VENDOR_CSS_CT = "text/css; charset=utf-8"
10068
  # Allowlist of the 7 keepers + KaTeX (exact filenames the console references).
@@ -10255,6 +10369,14 @@ except Exception as _opw_e: # never crash the app — additive only
10255
  # ---------------------------------------------------------------------------
10256
 
10257
 
 
 
 
 
 
 
 
 
10258
 
10259
  @app.get("/")
10260
  async def spa_root():
@@ -13288,11 +13410,11 @@ try:
13288
 
13289
  _a11oy_source_observation = {
13290
  "repository": "szl-holdings/a11oy",
13291
- "commit": "adac37574f88a30ff099f3ec7f548685d4166e6f",
13292
  "path": "",
13293
  "relation": "declared-source-with-hf-overlay",
13294
  "state": "VERIFIED_REFERENCE",
13295
- "evidence_url": "https://github.com/szl-holdings/a11oy/commit/adac37574f88a30ff099f3ec7f548685d4166e6f",
13296
  }
13297
  _szl_source_result = _szl_source_attestation.register(
13298
  app,
 
101
  # --- end OTel preamble ---
102
 
103
 
104
+ # The implicit FastAPI /openapi.json route calls app.openapi() without the
105
+ # defensive per-route fallback used by our backend hardening. On the assembled
106
+ # app one malformed legacy annotation could therefore make the conventional
107
+ # schema path return 500 while the curated schema remained healthy. Disable the
108
+ # implicit docs routes; szl_be_hardening registers both the canonical
109
+ # /api/a11oy/openapi.json and an exact /openapi.json alias from one builder.
110
+ app = FastAPI(
111
+ title="a11oy — Brand Orchestration Layer",
112
+ version="2.0.0",
113
+ openapi_url=None,
114
+ docs_url=None,
115
+ redoc_url=None,
116
+ )
117
+
118
+ # Waqay Security Loop (wave 15): expose only the deterministic, read-only
119
+ # contract. The implementation has no deployment, recall, repository, signing,
120
+ # model, or other external effector. Mutation endpoints stay deliberately
121
+ # absent until identity, policy, approval, provenance, and direct-origin bypass
122
+ # controls are independently verified.
123
+ try:
124
+ from szl_waqay_security_loop import security_loop_manifest
125
+ _WAQAY_SECURITY_LOOP_READY = True
126
+ except Exception: # pragma: no cover - honest optional degradation
127
+ security_loop_manifest = None # type: ignore[assignment]
128
+ _WAQAY_SECURITY_LOOP_READY = False
129
+
130
+
131
+ @app.get("/api/a11oy/v1/waqay/security-loop/manifest")
132
+ async def waqay_security_loop_manifest() -> JSONResponse:
133
+ if not _WAQAY_SECURITY_LOOP_READY or security_loop_manifest is None:
134
+ return JSONResponse(
135
+ {
136
+ "ready": False,
137
+ "mode": "UNAVAILABLE",
138
+ "effectors": 0,
139
+ "external_mutations": "DISABLED",
140
+ },
141
+ status_code=503,
142
+ )
143
+ return JSONResponse({"ready": True, **security_loop_manifest()})
144
+
145
+
146
+ # Claim Rupture Gate (wave 15): contract-only exposure. The module consumes
147
+ # externally supplied uncertainty/factuality evidence but never invents a score,
148
+ # upgrades a claim, persists a decision, or invokes an effector. Evaluation
149
+ # POST routes remain absent pending identity/policy/abuse review.
150
+ try:
151
+ from szl_claim_rupture_gate import info as claim_rupture_gate_info
152
+ _CLAIM_RUPTURE_GATE_READY = True
153
+ except Exception: # pragma: no cover - honest optional degradation
154
+ claim_rupture_gate_info = None # type: ignore[assignment]
155
+ _CLAIM_RUPTURE_GATE_READY = False
156
+
157
+
158
+ @app.get("/api/a11oy/v1/claim-integrity/info")
159
+ async def claim_integrity_info() -> JSONResponse:
160
+ if not _CLAIM_RUPTURE_GATE_READY or claim_rupture_gate_info is None:
161
+ return JSONResponse(
162
+ {
163
+ "ready": False,
164
+ "decision_state": "UNAVAILABLE",
165
+ "effectors_enabled": 0,
166
+ },
167
+ status_code=503,
168
+ )
169
+ return JSONResponse({"ready": True, **claim_rupture_gate_info()})
170
+
171
+
172
+ # Primary-project registry (wave 15): primary sources, projects/organizations
173
+ # rather than editorial rankings of people. The public endpoints intentionally
174
+ # serve the deterministic registry only; anonymous GitHub refreshes stay out of
175
+ # the request path to avoid rate-limit and availability claims.
176
+ try:
177
+ from research.a11oy_primary_project_registry import (
178
+ info as primary_project_registry_info,
179
+ snapshot as primary_project_registry_snapshot,
180
+ )
181
+ _PRIMARY_PROJECT_REGISTRY_READY = True
182
+ except Exception: # pragma: no cover - honest optional degradation
183
+ primary_project_registry_info = None # type: ignore[assignment]
184
+ primary_project_registry_snapshot = None # type: ignore[assignment]
185
+ _PRIMARY_PROJECT_REGISTRY_READY = False
186
+
187
+
188
+ @app.get("/api/a11oy/v1/frontier/projects/info")
189
+ async def frontier_projects_info() -> JSONResponse:
190
+ if not _PRIMARY_PROJECT_REGISTRY_READY or primary_project_registry_info is None:
191
+ return JSONResponse({"ok": False, "label": "UNAVAILABLE"}, status_code=503)
192
+ return JSONResponse(primary_project_registry_info())
193
+
194
+
195
+ @app.get("/api/a11oy/v1/frontier/projects")
196
+ async def frontier_projects_snapshot() -> JSONResponse:
197
+ if not _PRIMARY_PROJECT_REGISTRY_READY or primary_project_registry_snapshot is None:
198
+ return JSONResponse({"ok": False, "label": "UNAVAILABLE"}, status_code=503)
199
+ return JSONResponse(primary_project_registry_snapshot(fetch_live=False))
200
+
201
+
202
+ def _optional_module_absent(exc: Exception, module: str, surface: str,
203
+ *, stream=None) -> bool:
204
+ """Log a direct optional-module absence without a noisy traceback.
205
+
206
+ Missing transitive dependencies and registration errors deliberately return
207
+ False: those are defects and callers must retain their diagnostic traceback.
208
+ """
209
+ if isinstance(exc, ModuleNotFoundError) and getattr(exc, "name", None) == module:
210
+ print(
211
+ f"[a11oy] OPTIONAL-ABSENT {module}: {surface} not registered; "
212
+ "existing routes unaffected",
213
+ file=stream or sys.stderr,
214
+ )
215
+ return True
216
+ return False
217
 
218
  # ── Shared guarded-surface wrapper (Wave R Dev 2) — one bad surface must NEVER 500 the
219
  # SPA. This ASGI middleware converts an unhandled exception from any JSON API surface
 
328
  # fail-open (tracing NEVER breaks a request); locked=8; Λ=Conjecture 1; no key.
329
  try:
330
  import szl_observability as _szl_observability
331
+ _szl_obs_paths = _szl_observability.register(app, ns="a11oy")
332
+ print(f"[a11oy] Observability registered once: {_szl_obs_paths}", file=sys.stderr)
 
 
 
 
 
 
 
333
  except Exception as _szl_obs_e: # pragma: no cover
334
+ print(f"[a11oy] Observability NOT registered: {_szl_obs_e!r}; existing routes unaffected", file=sys.stderr)
335
  # ── Observability / distributed tracing (devN) — szl_observability ── end
336
 
337
+ # The spend cap is independent of tracing. An optional observability failure must
338
+ # never decide whether the free-first guardrail is present.
339
+ try:
340
+ import szl_spend_cap as _szl_spend_cap
341
+ print("[a11oy] " + _szl_spend_cap.register(app, ns="a11oy"), file=sys.stderr)
342
+ except Exception as _szl_spend_cap_e:
343
+ print(f"[szl_spend_cap] NOT mounted ({_szl_spend_cap_e!r}); existing routes unaffected", file=sys.stderr)
344
+
345
  # ── Resilience (devM) — szl_resilience ──
346
  # ADDITIVE on top of backend-hardening (#346) + prod-hardening (#345). Two proven
347
  # patterns, our own implementation: (1) a Hystrix-style CIRCUIT BREAKER that wraps
 
2999
  print(f"[a11oy] /about/thesis registered: {_thesis_status}", file=_sys_th.stderr)
3000
  except Exception as _th_e:
3001
  import sys as _sys_th, traceback as _tb_th
3002
+ if not _optional_module_absent(_th_e, "szl_thesis_about", "/about/thesis",
3003
+ stream=_sys_th.stderr):
3004
+ print(f"[a11oy] /about/thesis NOT registered: {_th_e}", file=_sys_th.stderr)
3005
+ _tb_th.print_exc()
3006
  # ── end /about/thesis ────────────────────────────────────────────────────────
3007
 
3008
  # ── Provenance Hardening (Yachay / Doctrine v12) — ADDITIVE, registered EARLY ──
 
3424
  app.include_router(_formulas_mod.router)
3425
  print("[a11oy] SZL_FORMULA_OPS mounted: 8x POST /formulas/<name> + /formulas-ops (UI) + /api/a11oy/v1/formulas-ops (index)", file=sys.stderr)
3426
  except Exception as _formulas_exc: # additive: never break the Space if the module is absent
3427
+ if not _optional_module_absent(
3428
+ _formulas_exc, "szl_formula_ops", "Formula Operationalizer"):
3429
+ print(f"[a11oy] FORMULAS mount failed: {_formulas_exc}", file=sys.stderr)
3430
 
3431
  # ---------------------------------------------------------------------------
3432
  # ADDITIVE (Yachay / Doctrine v12 PURIQ): mount the a11oy.code conversational
 
3987
  print(f"[a11oy] v4 PAC-Bayes Predict registered: {_v4_predict_status}", file=_v4p_sys.stderr)
3988
  except Exception as _v4p_e:
3989
  import sys as _v4p_sys, traceback as _v4p_tb
3990
+ if not _optional_module_absent(_v4p_e, "a11oy_v4_predict", "v4 PAC-Bayes Predict",
3991
+ stream=_v4p_sys.stderr):
3992
+ print(f"[a11oy] v4 PAC-Bayes Predict NOT registered: {_v4p_e!r}", file=_v4p_sys.stderr)
3993
+ _v4p_tb.print_exc()
3994
 
3995
  try:
3996
  import a11oy_v4_thesis_primitives as _v4_tp
 
3999
  print(f"[a11oy] v4 Thesis Primitives registered: {_v4_tp_status}", file=_v4tp_sys.stderr)
4000
  except Exception as _v4tp_e:
4001
  import sys as _v4tp_sys, traceback as _v4tp_tb
4002
+ if not _optional_module_absent(
4003
+ _v4tp_e, "a11oy_v4_thesis_primitives", "v4 Thesis Primitives",
4004
+ stream=_v4tp_sys.stderr):
4005
+ print(f"[a11oy] v4 Thesis Primitives NOT registered: {_v4tp_e!r}", file=_v4tp_sys.stderr)
4006
+ _v4tp_tb.print_exc()
4007
  # --- end PAC-Bayes Predict + Thesis Primitives ---
4008
 
4009
 
 
5040
  print(f"[a11oy] szl_sovereign_compute NOT registered ({_sc_e!r}); existing routes unaffected", file=sys.stderr)
5041
 
5042
 
5043
+ # Observability is registered exactly once near app construction. A historical
5044
+ # duplicate lived here and expected a dict even though register() returns
5045
+ # list[str], producing a false startup error after the routes were already live.
5046
+ # Keep one authoritative v1 tracing contract; the stale, nonexistent v3 claims
5047
+ # are intentionally removed rather than preserved as dead documentation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5048
 
5049
 
5050
  # ===========================================================================
 
5233
  print("[a11oy] szl_kernels_organ: 9 living kernels at /api/a11oy/v3/kernels/*", file=sys.stderr)
5234
  except Exception as _ke:
5235
  import traceback as _tb_k
5236
+ if not _optional_module_absent(_ke, "szl_kernels_organ", "agentic Codex kernels"):
5237
+ print(f"[a11oy] szl_kernels_organ NOT registered: {_ke}", file=sys.stderr)
5238
+ _tb_k.print_exc()
5239
 
5240
 
5241
  # ---------------------------------------------------------------------------
 
5330
  print(f"[a11oy] Typed Ontology + Object Explorer registered: {_ont_status}", file=sys.stderr)
5331
  except Exception as _ont_e:
5332
  import traceback as _ont_tb
5333
+ if not _optional_module_absent(_ont_e, "a11oy_ontology", "Typed Ontology"):
5334
+ print(f"[a11oy] Typed Ontology NOT registered: {_ont_e!r}", file=sys.stderr)
5335
+ _ont_tb.print_exc(file=sys.stderr)
5336
 
5337
  # C. Derivation DAG renderer (/api/a11oy/v4/derivation/{id}, /derivation/{id}, vendored Three.js)
5338
  try:
 
5341
  print(f"[a11oy] Derivation DAG renderer registered: {_deriv_status}", file=sys.stderr)
5342
  except Exception as _deriv_e:
5343
  import traceback as _deriv_tb
5344
+ if not _optional_module_absent(_deriv_e, "a11oy_derivation", "Derivation DAG"):
5345
+ print(f"[a11oy] Derivation DAG NOT registered: {_deriv_e!r}", file=sys.stderr)
5346
+ _deriv_tb.print_exc(file=sys.stderr)
5347
 
5348
  # B. Synchronized 4-lens shell (/explorer)
5349
  try:
 
5352
  print(f"[a11oy] 4-lens synchronized Explorer registered: {_explorer_status}", file=sys.stderr)
5353
  except Exception as _explorer_e:
5354
  import traceback as _explorer_tb
5355
+ if not _optional_module_absent(_explorer_e, "a11oy_explorer", "4-lens Explorer"):
5356
+ print(f"[a11oy] 4-lens Explorer NOT registered: {_explorer_e!r}", file=sys.stderr)
5357
+ _explorer_tb.print_exc(file=sys.stderr)
5358
 
5359
  # Every /agent/ask and /predict call writes the full Worker->Critic->Yuyay-13->Lambda->
5360
  # Khipu derivation chain into the Khipu (Receipt) store as a graph (Palantir AIP Logic
 
10170
  # ===========================================================================
10171
  try:
10172
  from fastapi.responses import Response as _VendResponse
10173
+ # Resolve from the image root in production and from this checkout during
10174
+ # local operator verification. The previous image-only path made the
10175
+ # injected operator widget 404 on every local HTML page even though the
10176
+ # vendored asset was present in ``static-vendor/``.
10177
  _VENDOR_DIR = Path("/app/static-vendor")
10178
+ if not _VENDOR_DIR.is_dir():
10179
+ _VENDOR_DIR = Path(__file__).resolve().parent / "static-vendor"
10180
  _VENDOR_JS_CT = "application/javascript; charset=utf-8"
10181
  _VENDOR_CSS_CT = "text/css; charset=utf-8"
10182
  # Allowlist of the 7 keepers + KaTeX (exact filenames the console references).
 
10369
  # ---------------------------------------------------------------------------
10370
 
10371
 
10372
+ @app.get("/favicon.ico", include_in_schema=False)
10373
+ async def favicon_no_content() -> Response:
10374
+ """Avoid sending the browser's implicit favicon request into the SPA
10375
+ fallback. This build has no canonical ICO asset, so an honest 204 is
10376
+ preferable to a fabricated icon or the previous local-dev 500."""
10377
+ return Response(status_code=204, headers={"Cache-Control": "public, max-age=86400"})
10378
+
10379
+
10380
 
10381
  @app.get("/")
10382
  async def spa_root():
 
13410
 
13411
  _a11oy_source_observation = {
13412
  "repository": "szl-holdings/a11oy",
13413
+ "commit": "2ca22d0b337805a2d4e7e65af3f6738c401431a4",
13414
  "path": "",
13415
  "relation": "declared-source-with-hf-overlay",
13416
  "state": "VERIFIED_REFERENCE",
13417
+ "evidence_url": "https://github.com/szl-holdings/a11oy/commit/2ca22d0b337805a2d4e7e65af3f6738c401431a4",
13418
  }
13419
  _szl_source_result = _szl_source_attestation.register(
13420
  app,
szl3d_holographic.py CHANGED
@@ -38,6 +38,7 @@ from urllib.request import Request, urlopen
38
 
39
  # Surface slots (id, human title) — the frontier tier + the 9 estate surfaces.
40
  SURFACES: List[Dict[str, str]] = [
 
41
  {"id": "atlas", "cat": "map", "flag": True, "title": "Atlas", "owner": "Wave27"},
42
  {"id": "frontier", "cat": "map", "title": "Frontier", "owner": "Dev0"},
43
  {"id": "neuromorphic", "cat": "more", "title": "Neuromorphic", "owner": "Dev0"},
 
38
 
39
  # Surface slots (id, human title) — the frontier tier + the 9 estate surfaces.
40
  SURFACES: List[Dict[str, str]] = [
41
+ {"id": "integritycontrol", "cat": "governance", "title": "Integrity Control Plane", "owner": "Wave15"},
42
  {"id": "atlas", "cat": "map", "flag": True, "title": "Atlas", "owner": "Wave27"},
43
  {"id": "frontier", "cat": "map", "title": "Frontier", "owner": "Dev0"},
44
  {"id": "neuromorphic", "cat": "more", "title": "Neuromorphic", "owner": "Dev0"},
szl_be_hardening.py CHANGED
@@ -276,6 +276,11 @@ class DurableKhipu:
276
  self._lock = threading.RLock()
277
  self.backend = "memory"
278
  self._mem: List[Dict[str, Any]] = []
 
 
 
 
 
279
  self._db: Optional[sqlite3.Connection] = None
280
  self._json_path: Optional[str] = None
281
 
@@ -287,14 +292,13 @@ class DurableKhipu:
287
 
288
  try:
289
  os.makedirs(os.path.dirname(self._path), exist_ok=True)
290
- self._db = sqlite3.connect(self._path, check_same_thread=False)
291
- self._db.execute(
292
- "CREATE TABLE IF NOT EXISTS khipu ("
293
- "seq INTEGER PRIMARY KEY, action TEXT NOT NULL, "
294
- "payload TEXT NOT NULL, prev TEXT NOT NULL, "
295
- "digest TEXT NOT NULL, ts REAL NOT NULL)"
296
- )
297
- self._db.commit()
298
  self.backend = "sqlite"
299
  except Exception: # disk unwritable -> JSON file fallback
300
  try:
@@ -313,21 +317,24 @@ class DurableKhipu:
313
  return hashlib.sha3_256(raw).hexdigest()
314
 
315
  def _all(self) -> List[Dict[str, Any]]:
316
- if self.backend == "sqlite" and self._db is not None:
317
- cur = self._db.execute(
318
- "SELECT seq, action, payload, prev, digest, ts FROM khipu ORDER BY seq"
319
- )
 
 
320
  return [
321
  {"seq": r[0], "action": r[1], "payload": json.loads(r[2]),
322
  "prev": r[3], "digest": r[4], "ts": r[5]}
323
- for r in cur.fetchall()
324
  ]
325
  return list(self._mem)
326
 
327
  def count(self) -> int:
328
  with self._lock:
329
- if self.backend == "sqlite" and self._db is not None:
330
- return int(self._db.execute("SELECT COUNT(*) FROM khipu").fetchone()[0])
 
331
  return len(self._mem)
332
 
333
  def head(self) -> str:
@@ -338,22 +345,36 @@ class DurableKhipu:
338
  def emit(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
339
  payload = payload or {}
340
  with self._lock:
341
- rows = self._all()
342
- prev = rows[-1]["digest"] if rows else _GENESIS
343
- seq = len(rows)
344
- body = {"organ": self.organ, "ns": self.ns, "seq": seq,
345
- "action": action, "payload": payload, "prev": prev}
346
- digest = self._digest(body)
347
- ts = time.time()
348
- rec = dict(body, digest=digest, ts=ts)
349
- if self.backend == "sqlite" and self._db is not None:
350
- self._db.execute(
351
- "INSERT INTO khipu(seq, action, payload, prev, digest, ts) "
352
- "VALUES (?,?,?,?,?,?)",
353
- (seq, action, json.dumps(payload, sort_keys=True), prev, digest, ts),
354
- )
355
- self._db.commit()
 
 
 
 
 
 
356
  else:
 
 
 
 
 
 
 
 
357
  self._mem.append(rec)
358
  if self.backend == "json" and self._json_path:
359
  with open(self._json_path, "w") as fh:
@@ -609,6 +630,7 @@ def harden(app: Any, organ: str, ns: Optional[str] = None,
609
  # Gradio whose combined schema generation can raise; in that case we fall
610
  # back to FastAPI's get_openapi over THIS app's own APIRoutes (still a real,
611
  # auto-generated spec — never a hand-written stub).
 
612
  @app.get(f"/api/{organ}/openapi.json", include_in_schema=False)
613
  async def _organ_openapi():
614
  try:
@@ -642,7 +664,8 @@ def harden(app: Any, organ: str, ns: Optional[str] = None,
642
  status_code=500,
643
  )
644
 
645
- report["registered"].append(f"openapi:/api/{organ}/openapi.json")
 
646
 
647
  # ---- 9: honest footer -------------------------------------------------
648
  @app.get("/honest", tags=["doctrine"])
 
276
  self._lock = threading.RLock()
277
  self.backend = "memory"
278
  self._mem: List[Dict[str, Any]] = []
279
+ # SQLite connections are deliberately short-lived. A process-global
280
+ # connection kept the database file locked on Windows and made clean
281
+ # restart/deployment tests fail at directory teardown. Each operation
282
+ # opens a transaction-scoped connection instead; the on-disk chain is the
283
+ # state, not a connection object.
284
  self._db: Optional[sqlite3.Connection] = None
285
  self._json_path: Optional[str] = None
286
 
 
292
 
293
  try:
294
  os.makedirs(os.path.dirname(self._path), exist_ok=True)
295
+ with sqlite3.connect(self._path, timeout=30.0) as db:
296
+ db.execute(
297
+ "CREATE TABLE IF NOT EXISTS khipu ("
298
+ "seq INTEGER PRIMARY KEY, action TEXT NOT NULL, "
299
+ "payload TEXT NOT NULL, prev TEXT NOT NULL, "
300
+ "digest TEXT NOT NULL, ts REAL NOT NULL)"
301
+ )
 
302
  self.backend = "sqlite"
303
  except Exception: # disk unwritable -> JSON file fallback
304
  try:
 
317
  return hashlib.sha3_256(raw).hexdigest()
318
 
319
  def _all(self) -> List[Dict[str, Any]]:
320
+ if self.backend == "sqlite":
321
+ with sqlite3.connect(self._path, timeout=30.0) as db:
322
+ rows = db.execute(
323
+ "SELECT seq, action, payload, prev, digest, ts "
324
+ "FROM khipu ORDER BY seq"
325
+ ).fetchall()
326
  return [
327
  {"seq": r[0], "action": r[1], "payload": json.loads(r[2]),
328
  "prev": r[3], "digest": r[4], "ts": r[5]}
329
+ for r in rows
330
  ]
331
  return list(self._mem)
332
 
333
  def count(self) -> int:
334
  with self._lock:
335
+ if self.backend == "sqlite":
336
+ with sqlite3.connect(self._path, timeout=30.0) as db:
337
+ return int(db.execute("SELECT COUNT(*) FROM khipu").fetchone()[0])
338
  return len(self._mem)
339
 
340
  def head(self) -> str:
 
345
  def emit(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
346
  payload = payload or {}
347
  with self._lock:
348
+ if self.backend == "sqlite":
349
+ # BEGIN IMMEDIATE serializes concurrent writers, including two
350
+ # independently-opened stores after a rolling process restart.
351
+ with sqlite3.connect(self._path, timeout=30.0) as db:
352
+ db.execute("BEGIN IMMEDIATE")
353
+ last = db.execute(
354
+ "SELECT seq, digest FROM khipu ORDER BY seq DESC LIMIT 1"
355
+ ).fetchone()
356
+ seq = int(last[0]) + 1 if last else 0
357
+ prev = str(last[1]) if last else _GENESIS
358
+ body = {"organ": self.organ, "ns": self.ns, "seq": seq,
359
+ "action": action, "payload": payload, "prev": prev}
360
+ digest = self._digest(body)
361
+ ts = time.time()
362
+ rec = dict(body, digest=digest, ts=ts)
363
+ db.execute(
364
+ "INSERT INTO khipu(seq, action, payload, prev, digest, ts) "
365
+ "VALUES (?,?,?,?,?,?)",
366
+ (seq, action, json.dumps(payload, sort_keys=True), prev, digest, ts),
367
+ )
368
+ return rec
369
  else:
370
+ rows = self._all()
371
+ prev = rows[-1]["digest"] if rows else _GENESIS
372
+ seq = len(rows)
373
+ body = {"organ": self.organ, "ns": self.ns, "seq": seq,
374
+ "action": action, "payload": payload, "prev": prev}
375
+ digest = self._digest(body)
376
+ ts = time.time()
377
+ rec = dict(body, digest=digest, ts=ts)
378
  self._mem.append(rec)
379
  if self.backend == "json" and self._json_path:
380
  with open(self._json_path, "w") as fh:
 
630
  # Gradio whose combined schema generation can raise; in that case we fall
631
  # back to FastAPI's get_openapi over THIS app's own APIRoutes (still a real,
632
  # auto-generated spec — never a hand-written stub).
633
+ @app.get("/openapi.json", include_in_schema=False)
634
  @app.get(f"/api/{organ}/openapi.json", include_in_schema=False)
635
  async def _organ_openapi():
636
  try:
 
664
  status_code=500,
665
  )
666
 
667
+ report["registered"].append(
668
+ f"openapi:/api/{organ}/openapi.json+alias:/openapi.json")
669
 
670
  # ---- 9: honest footer -------------------------------------------------
671
  @app.get("/honest", tags=["doctrine"])
szl_claim_rupture_gate.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """SZL Claim Rupture Gate -- fail-closed claim-integrity assessment.
4
+
5
+ This module is deliberately smaller than a fact-checker. It does not browse, retrieve,
6
+ infer truth, resolve contradictions, or calculate semantic uncertainty. It accepts
7
+ explicit claim atoms, evidence/provenance references, accountable consequence owners,
8
+ and *externally supplied* factuality/uncertainty signals. It then applies a transparent,
9
+ deterministic rubric that decides whether a claim may be carried forward for human review.
10
+
11
+ The contract is honest by construction:
12
+
13
+ * raw prose atomization is STRUCTURAL-SPLIT-ONLY and must be human reviewed;
14
+ * a claimed evidence label is never upgraded;
15
+ * missing provenance, ownership, malformed signals, or contradictions fail closed;
16
+ * VERIFIED requires traceable verification for every evidence reference plus an external,
17
+ traceable VERIFIED factuality signal;
18
+ * every result is PROPOSAL_ONLY with zero effectors;
19
+ * receipts are unsigned SHA-256 content digests, not signatures or truth certificates.
20
+
21
+ This adds no theorem and changes no locked formula. It is pure Python stdlib and performs
22
+ no I/O, persistence, networking, authentication, signing, or mutation.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import json
29
+ import re
30
+ from typing import Any, Iterable, Mapping, Sequence
31
+
32
+
33
+ VERIFIED = "VERIFIED"
34
+ SUPPORTED = "SUPPORTED"
35
+ UNCERTAIN = "UNCERTAIN"
36
+ REFUTED = "REFUTED"
37
+ UNKNOWN = "UNKNOWN"
38
+ CLAIM_STATES = (VERIFIED, SUPPORTED, UNCERTAIN, REFUTED, UNKNOWN)
39
+
40
+ PROPOSAL_ONLY = "PROPOSAL_ONLY"
41
+ NO_EFFECTORS = 0
42
+ SEMANTIC_UNCERTAINTY_ABSTAIN_THRESHOLD = 0.66
43
+ MODULE_ID = "szl-claim-rupture-gate"
44
+ CONTRACT_VERSION = "1.0.0"
45
+
46
+ _SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
47
+ _SPLIT_RE = re.compile(r"(?:\r?\n)+|(?<=[.!?;])\s+")
48
+
49
+
50
+ # Open, inspectable error/abstention rubric. Codes are stable API data, not prose-only
51
+ # documentation. Integrators may display them verbatim and tests pin their semantics.
52
+ ERROR_RUBRIC: dict[str, dict[str, Any]] = {
53
+ "RG-001": {
54
+ "condition": "missing or empty claim statement",
55
+ "default_state": UNKNOWN,
56
+ "abstain": True,
57
+ },
58
+ "RG-002": {
59
+ "condition": "claim atom has not been explicitly reviewed as atomic",
60
+ "default_state": UNKNOWN,
61
+ "abstain": True,
62
+ },
63
+ "RG-003": {
64
+ "condition": "no evidence reference supplied",
65
+ "default_state": UNKNOWN,
66
+ "abstain": True,
67
+ },
68
+ "RG-004": {
69
+ "condition": "evidence reference lacks traceable provenance",
70
+ "default_state": UNKNOWN,
71
+ "abstain": True,
72
+ },
73
+ "RG-005": {
74
+ "condition": "claim lacks an accountable consequence owner and scope",
75
+ "default_state": UNKNOWN,
76
+ "abstain": True,
77
+ },
78
+ "RG-006": {
79
+ "condition": "unresolved or confirmed contradiction affects the claim",
80
+ "default_state": UNCERTAIN,
81
+ "abstain": True,
82
+ },
83
+ "RG-007": {
84
+ "condition": "traceable evidence or factuality signal explicitly refutes claim",
85
+ "default_state": REFUTED,
86
+ "abstain": True,
87
+ },
88
+ "RG-008": {
89
+ "condition": "external semantic-uncertainty or factuality signal is malformed or untraceable",
90
+ "default_state": UNKNOWN,
91
+ "abstain": True,
92
+ },
93
+ "RG-009": {
94
+ "condition": "externally supplied semantic uncertainty meets abstention threshold",
95
+ "default_state": UNCERTAIN,
96
+ "abstain": True,
97
+ },
98
+ "RG-010": {
99
+ "condition": "evidence is inconclusive or explicitly uncertain",
100
+ "default_state": UNCERTAIN,
101
+ "abstain": True,
102
+ },
103
+ }
104
+
105
+
106
+ def _canonical_json(value: Any) -> str:
107
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
108
+
109
+
110
+ def _digest(value: Any) -> str:
111
+ return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
112
+
113
+
114
+ def _nonempty(value: Any) -> bool:
115
+ return isinstance(value, str) and bool(value.strip())
116
+
117
+
118
+ def _stable_claim_id(statement: str, index: int) -> str:
119
+ # Missing caller IDs are not silently accepted. This identifier merely lets the
120
+ # rejection be addressed deterministically in the response.
121
+ material = {"index": index, "statement": statement}
122
+ return f"unidentified-{_digest(material)[:16]}"
123
+
124
+
125
+ def _provenance_complete(provenance: Any) -> bool:
126
+ """Traceability minimum: source identity plus digest or immutable receipt reference.
127
+
128
+ This validates shape only. It does *not* dereference, replay, or cryptographically
129
+ verify the asserted provenance.
130
+ """
131
+ if not isinstance(provenance, Mapping):
132
+ return False
133
+ if not _nonempty(provenance.get("source_id")):
134
+ return False
135
+ digest = provenance.get("content_sha256")
136
+ receipt = provenance.get("receipt_ref")
137
+ return bool((_nonempty(digest) and _SHA256_RE.fullmatch(digest.strip()))
138
+ or _nonempty(receipt))
139
+
140
+
141
+ def _owner_complete(owner: Any) -> bool:
142
+ return (isinstance(owner, Mapping)
143
+ and _nonempty(owner.get("owner_id"))
144
+ and _nonempty(owner.get("accountability_scope")))
145
+
146
+
147
+ def atomize_text(text: str) -> dict[str, Any]:
148
+ """Produce deterministic *candidate* atoms from visible punctuation/newlines only.
149
+
150
+ This is not semantic atomization. Each candidate is ``atomic=False`` and therefore
151
+ fails closed until a human explicitly reviews it and supplies evidence/ownership.
152
+ """
153
+ source = text if isinstance(text, str) else ""
154
+ pieces = [p.strip(" \t-*\u2022") for p in _SPLIT_RE.split(source) if p.strip(" \t-*\u2022")]
155
+ atoms = []
156
+ for index, statement in enumerate(pieces):
157
+ atoms.append({
158
+ "claim_id": f"candidate-{index + 1:04d}-{_digest(statement)[:12]}",
159
+ "statement": statement,
160
+ "atomic": False,
161
+ "atomization_state": "STRUCTURAL-SPLIT-ONLY",
162
+ "human_review_required": True,
163
+ "evidence_refs": [],
164
+ "consequence_owner": None,
165
+ })
166
+ return {
167
+ "module": MODULE_ID,
168
+ "contract_version": CONTRACT_VERSION,
169
+ "method": "VISIBLE-PUNCTUATION-AND-NEWLINE-SPLIT",
170
+ "semantic_atomization_computed": False,
171
+ "decision_state": PROPOSAL_ONLY,
172
+ "effectors_enabled": NO_EFFECTORS,
173
+ "candidate_count": len(atoms),
174
+ "atoms": atoms,
175
+ }
176
+
177
+
178
+ def _validate_semantic_signal(signal: Any) -> tuple[dict[str, Any] | None, str | None]:
179
+ if signal is None:
180
+ return None, None
181
+ if not isinstance(signal, Mapping):
182
+ return None, "RG-008"
183
+ value = signal.get("value")
184
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
185
+ return None, "RG-008"
186
+ if not 0.0 <= float(value) <= 1.0:
187
+ return None, "RG-008"
188
+ if not _nonempty(signal.get("source_ref")) or not _nonempty(signal.get("method")):
189
+ return None, "RG-008"
190
+ return {
191
+ "value": float(value),
192
+ "source_ref": signal["source_ref"].strip(),
193
+ "method": signal["method"].strip(),
194
+ "computed_by_gate": False,
195
+ }, None
196
+
197
+
198
+ def _validate_factuality_signal(signal: Any) -> tuple[dict[str, Any] | None, str | None]:
199
+ if signal is None:
200
+ return None, None
201
+ if not isinstance(signal, Mapping):
202
+ return None, "RG-008"
203
+ state = str(signal.get("state", "")).strip().upper()
204
+ if state not in CLAIM_STATES:
205
+ return None, "RG-008"
206
+ if not _nonempty(signal.get("source_ref")) or not _nonempty(signal.get("method")):
207
+ return None, "RG-008"
208
+ return {
209
+ "state": state,
210
+ "source_ref": signal["source_ref"].strip(),
211
+ "method": signal["method"].strip(),
212
+ "computed_by_gate": False,
213
+ }, None
214
+
215
+
216
+ def _contradiction_effects(claim_ids: set[str], contradictions: Any) -> dict[str, list[dict[str, Any]]]:
217
+ effects: dict[str, list[dict[str, Any]]] = {cid: [] for cid in claim_ids}
218
+ if contradictions is None:
219
+ return effects
220
+ if not isinstance(contradictions, Sequence) or isinstance(contradictions, (str, bytes)):
221
+ for cid in effects:
222
+ effects[cid].append({"status": "MALFORMED", "rubric_code": "RG-008"})
223
+ return effects
224
+ for raw in contradictions:
225
+ if not isinstance(raw, Mapping):
226
+ for cid in effects:
227
+ effects[cid].append({"status": "MALFORMED", "rubric_code": "RG-008"})
228
+ continue
229
+ ids = raw.get("claim_ids")
230
+ ids = [str(x) for x in ids] if isinstance(ids, Sequence) and not isinstance(ids, (str, bytes)) else []
231
+ status = str(raw.get("status", "UNRESOLVED")).strip().upper()
232
+ traceable = _provenance_complete(raw.get("provenance"))
233
+ refutes = raw.get("refutes_claim_ids")
234
+ refutes = {str(x) for x in refutes} if isinstance(refutes, Sequence) and not isinstance(refutes, (str, bytes)) else set()
235
+ for cid in set(ids) & claim_ids:
236
+ if not traceable:
237
+ effects[cid].append({"status": status, "rubric_code": "RG-004"})
238
+ elif status in {"UNRESOLVED", "CONFIRMED"}:
239
+ effects[cid].append({
240
+ "status": status,
241
+ "rubric_code": "RG-007" if cid in refutes else "RG-006",
242
+ "provenance_traceable": True,
243
+ })
244
+ elif status == "RESOLVED" and not _nonempty(raw.get("resolution_ref")):
245
+ effects[cid].append({"status": status, "rubric_code": "RG-008"})
246
+ return effects
247
+
248
+
249
+ def _base_evidence_state(evidence_refs: Any) -> tuple[str, list[str], list[dict[str, Any]]]:
250
+ errors: list[str] = []
251
+ normalized: list[dict[str, Any]] = []
252
+ if not isinstance(evidence_refs, Sequence) or isinstance(evidence_refs, (str, bytes)) or not evidence_refs:
253
+ return UNKNOWN, ["RG-003"], normalized
254
+
255
+ states: list[str] = []
256
+ for raw in evidence_refs:
257
+ if not isinstance(raw, Mapping) or not _nonempty(raw.get("reference_id")):
258
+ errors.append("RG-004")
259
+ continue
260
+ state = str(raw.get("evidence_state", UNKNOWN)).strip().upper()
261
+ if state not in CLAIM_STATES:
262
+ errors.append("RG-008")
263
+ state = UNKNOWN
264
+ provenance_ok = _provenance_complete(raw.get("provenance"))
265
+ if not provenance_ok:
266
+ errors.append("RG-004")
267
+ verification_ref = raw.get("verification_ref")
268
+ if state == VERIFIED and not _nonempty(verification_ref):
269
+ # A bare VERIFIED string is not traceable verification.
270
+ errors.append("RG-004")
271
+ states.append(state)
272
+ normalized.append({
273
+ "reference_id": raw.get("reference_id"),
274
+ "evidence_state": state,
275
+ "provenance_traceable": provenance_ok,
276
+ "verification_ref": verification_ref if _nonempty(verification_ref) else None,
277
+ })
278
+
279
+ if "RG-004" in errors or "RG-008" in errors or not normalized:
280
+ return UNKNOWN, sorted(set(errors or ["RG-004"])), normalized
281
+ if REFUTED in states:
282
+ errors.append("RG-007")
283
+ return REFUTED, sorted(set(errors)), normalized
284
+ if UNKNOWN in states:
285
+ return UNKNOWN, sorted(set(errors)), normalized
286
+ if UNCERTAIN in states:
287
+ errors.append("RG-010")
288
+ return UNCERTAIN, sorted(set(errors)), normalized
289
+ if all(s == VERIFIED for s in states):
290
+ return VERIFIED, sorted(set(errors)), normalized
291
+ return SUPPORTED, sorted(set(errors)), normalized
292
+
293
+
294
+ def _state_min(a: str, b: str) -> str:
295
+ # Conservative partial order. Explicit refutation dominates, then missing evidence,
296
+ # then uncertainty; SUPPORTED and VERIFIED are successively stronger.
297
+ rank = {REFUTED: 0, UNKNOWN: 1, UNCERTAIN: 2, SUPPORTED: 3, VERIFIED: 4}
298
+ return a if rank[a] <= rank[b] else b
299
+
300
+
301
+ def _assess_atom(atom: Any, index: int, external: Any, contradictions: list[dict[str, Any]]) -> dict[str, Any]:
302
+ raw = atom if isinstance(atom, Mapping) else {}
303
+ statement = str(raw.get("statement", "")).strip()
304
+ claim_id = str(raw.get("claim_id", "")).strip() or _stable_claim_id(statement, index)
305
+ errors: list[str] = []
306
+
307
+ if not statement:
308
+ errors.append("RG-001")
309
+ if raw.get("atomic") is not True:
310
+ errors.append("RG-002")
311
+ if not _owner_complete(raw.get("consequence_owner")):
312
+ errors.append("RG-005")
313
+
314
+ state, evidence_errors, evidence = _base_evidence_state(raw.get("evidence_refs"))
315
+ errors.extend(evidence_errors)
316
+
317
+ ext = external if isinstance(external, Mapping) else {}
318
+ semantic, semantic_error = _validate_semantic_signal(ext.get("semantic_uncertainty"))
319
+ factuality, factuality_error = _validate_factuality_signal(ext.get("factuality"))
320
+ if semantic_error:
321
+ errors.append(semantic_error)
322
+ state = UNKNOWN
323
+ if factuality_error:
324
+ errors.append(factuality_error)
325
+ state = UNKNOWN
326
+
327
+ # Missing factuality never upgrades evidence. Even completely VERIFIED evidence is
328
+ # only SUPPORTED at the claim level without a traceable external factuality verdict.
329
+ if factuality is None and state == VERIFIED:
330
+ state = SUPPORTED
331
+ elif factuality is not None:
332
+ state = _state_min(state, factuality["state"])
333
+ if factuality["state"] == REFUTED:
334
+ errors.append("RG-007")
335
+ elif factuality["state"] == UNCERTAIN:
336
+ errors.append("RG-010")
337
+
338
+ if semantic is not None and semantic["value"] >= SEMANTIC_UNCERTAINTY_ABSTAIN_THRESHOLD:
339
+ if state not in {REFUTED, UNKNOWN}:
340
+ state = UNCERTAIN
341
+ errors.append("RG-009")
342
+
343
+ for effect in contradictions:
344
+ errors.append(effect["rubric_code"])
345
+ if effect["rubric_code"] == "RG-007":
346
+ state = REFUTED
347
+ elif effect["rubric_code"] in {"RG-004", "RG-008"}:
348
+ state = UNKNOWN
349
+ elif state not in {REFUTED, UNKNOWN}:
350
+ state = UNCERTAIN
351
+
352
+ # Structural/ownership/provenance failures always fail closed to UNKNOWN, except an
353
+ # explicit refutation which remains visible as the stronger negative verdict.
354
+ if any(code in errors for code in {"RG-001", "RG-002", "RG-003", "RG-004", "RG-005", "RG-008"}):
355
+ if state != REFUTED:
356
+ state = UNKNOWN
357
+
358
+ errors = sorted(set(errors))
359
+ abstain = state in {UNCERTAIN, REFUTED, UNKNOWN} or any(ERROR_RUBRIC[c]["abstain"] for c in errors)
360
+ return {
361
+ "claim_id": claim_id,
362
+ "statement": statement,
363
+ "state": state,
364
+ "abstain_required": abstain,
365
+ "rubric_codes": errors,
366
+ "evidence_refs": evidence,
367
+ "consequence_owner": dict(raw.get("consequence_owner")) if _owner_complete(raw.get("consequence_owner")) else None,
368
+ "external_signals": {
369
+ "semantic_uncertainty": semantic,
370
+ "factuality": factuality,
371
+ "computed_by_gate": [],
372
+ },
373
+ "contradictions": contradictions,
374
+ "decision_state": PROPOSAL_ONLY,
375
+ "effectors_enabled": NO_EFFECTORS,
376
+ }
377
+
378
+
379
+ def evaluate_claims(
380
+ claims: Iterable[Mapping[str, Any]],
381
+ *,
382
+ external_signals: Mapping[str, Mapping[str, Any]] | None = None,
383
+ contradictions: Sequence[Mapping[str, Any]] | None = None,
384
+ ) -> dict[str, Any]:
385
+ """Assess explicit claim atoms and return an unsigned, deterministic receipt.
386
+
387
+ ``external_signals`` is keyed by claim_id. Values are retained as externally supplied
388
+ facts about the signal source; this gate never produces semantic/factuality scores.
389
+ """
390
+ materialized = list(claims) if not isinstance(claims, (str, bytes, Mapping)) else []
391
+ ids = set()
392
+ for index, raw in enumerate(materialized):
393
+ if isinstance(raw, Mapping):
394
+ statement = str(raw.get("statement", "")).strip()
395
+ ids.add(str(raw.get("claim_id", "")).strip() or _stable_claim_id(statement, index))
396
+ contradiction_map = _contradiction_effects(ids, contradictions)
397
+ external_signals = external_signals if isinstance(external_signals, Mapping) else {}
398
+
399
+ assessments = []
400
+ for index, atom in enumerate(materialized):
401
+ raw = atom if isinstance(atom, Mapping) else {}
402
+ statement = str(raw.get("statement", "")).strip()
403
+ claim_id = str(raw.get("claim_id", "")).strip() or _stable_claim_id(statement, index)
404
+ assessments.append(_assess_atom(
405
+ atom,
406
+ index,
407
+ external_signals.get(claim_id),
408
+ contradiction_map.get(claim_id, []),
409
+ ))
410
+
411
+ counts = {state: sum(1 for row in assessments if row["state"] == state) for state in CLAIM_STATES}
412
+ if not assessments:
413
+ overall = UNKNOWN
414
+ abstain = True
415
+ else:
416
+ overall = assessments[0]["state"]
417
+ for row in assessments[1:]:
418
+ overall = _state_min(overall, row["state"])
419
+ abstain = any(row["abstain_required"] for row in assessments)
420
+
421
+ core = {
422
+ "module": MODULE_ID,
423
+ "contract_version": CONTRACT_VERSION,
424
+ "decision_state": PROPOSAL_ONLY,
425
+ "effectors_enabled": NO_EFFECTORS,
426
+ "overall_state": overall,
427
+ "abstain_required": abstain,
428
+ "gate_outcome": "ABSTAIN" if abstain else "EVIDENCE-COMPLETE-FOR-HUMAN-REVIEW",
429
+ "claim_count": len(assessments),
430
+ "state_counts": counts,
431
+ "claims": assessments,
432
+ "signal_contract": {
433
+ "semantic_uncertainty": "EXTERNALLY-SUPPLIED-ONLY",
434
+ "factuality": "EXTERNALLY-SUPPLIED-ONLY",
435
+ "semantic_threshold": SEMANTIC_UNCERTAINTY_ABSTAIN_THRESHOLD,
436
+ "computed_by_gate": [],
437
+ },
438
+ "honesty_invariants": {
439
+ "no_truth_inference": True,
440
+ "no_contradiction_resolution": True,
441
+ "missing_provenance_fails_closed": True,
442
+ "all_outputs_proposal_only": True,
443
+ "effectors_are_zero": True,
444
+ },
445
+ }
446
+ return {
447
+ **core,
448
+ "receipt": {
449
+ "mode": "UNSIGNED-CONTENT-DIGEST",
450
+ "algorithm": "sha256",
451
+ "signed": False,
452
+ "content_sha256": _digest(core),
453
+ "attests_truth": False,
454
+ },
455
+ }
456
+
457
+
458
+ def info() -> dict[str, Any]:
459
+ """Static contract description for a future additive API registration."""
460
+ return {
461
+ "module": MODULE_ID,
462
+ "contract_version": CONTRACT_VERSION,
463
+ "states": list(CLAIM_STATES),
464
+ "decision_state": PROPOSAL_ONLY,
465
+ "effectors_enabled": NO_EFFECTORS,
466
+ "rubric": ERROR_RUBRIC,
467
+ "intended_read_only_api": [
468
+ {"method": "GET", "path": "/api/a11oy/v1/claim-integrity/info", "mutates": False},
469
+ {"method": "POST", "path": "/api/a11oy/v1/claim-integrity/atomize", "mutates": False,
470
+ "note": "computational read; emits candidates only"},
471
+ {"method": "POST", "path": "/api/a11oy/v1/claim-integrity/evaluate", "mutates": False,
472
+ "note": "computational read; unsigned digest only"},
473
+ ],
474
+ "not_implemented_here": ["HTTP registration", "persistence", "signing", "effectors"],
475
+ }
476
+
szl_hub.py CHANGED
@@ -217,22 +217,21 @@ def register(app: FastAPI) -> None:
217
  FastAPI's built-in Swagger UI also defaults to /docs and is registered first
218
  (at app construction), so it would otherwise shadow our tab. To keep BOTH and
219
  stay additive (zero regression), we relocate the OpenAPI/Swagger UI to /api/docs
220
- and the raw schema to /api/openapi.json, then drop the original /docs + /redoc
221
- + /openapi.json default routes. Nothing customer-facing or SPA-facing is lost;
222
- the interactive API explorer simply moves under the /api/* namespace where it
223
- belongs.
224
  """
225
  # --- relocate FastAPI's default docs so /docs is free for the branded tab ---
226
  try:
227
  from fastapi.openapi.docs import get_swagger_ui_html
228
 
229
- _default_docs_paths = {"/docs", "/redoc", "/openapi.json"}
230
  app.router.routes = [
231
  r for r in app.router.routes
232
  if getattr(r, "path", None) not in _default_docs_paths
233
  ]
234
  # Re-expose the schema + Swagger UI under /api/* (additive, not lost).
235
- app.openapi_url = "/api/openapi.json"
236
 
237
  @app.get("/api/openapi.json", include_in_schema=False)
238
  async def _hub_openapi() -> JSONResponse:
@@ -279,7 +278,10 @@ def register(app: FastAPI) -> None:
279
 
280
  @app.get("/api/docs", include_in_schema=False)
281
  async def _hub_swagger():
282
- return get_swagger_ui_html(openapi_url="/api/openapi.json", title="a11oy API — Swagger UI")
 
 
 
283
  except Exception:
284
  # If relocation fails for any reason, fall through: the branded /docs tab
285
  # still registers below; worst case Swagger keeps /docs. Never fatal.
 
217
  FastAPI's built-in Swagger UI also defaults to /docs and is registered first
218
  (at app construction), so it would otherwise shadow our tab. To keep BOTH and
219
  stay additive (zero regression), we relocate the OpenAPI/Swagger UI to /api/docs
220
+ and keep /api/openapi.json as a legacy schema alias. The conventional
221
+ /openapi.json path is owned by backend hardening as an exact alias of the
222
+ curated /api/a11oy/openapi.json schema; this module must not delete it.
 
223
  """
224
  # --- relocate FastAPI's default docs so /docs is free for the branded tab ---
225
  try:
226
  from fastapi.openapi.docs import get_swagger_ui_html
227
 
228
+ _default_docs_paths = {"/docs", "/redoc"}
229
  app.router.routes = [
230
  r for r in app.router.routes
231
  if getattr(r, "path", None) not in _default_docs_paths
232
  ]
233
  # Re-expose the schema + Swagger UI under /api/* (additive, not lost).
234
+ app.openapi_url = "/api/a11oy/openapi.json"
235
 
236
  @app.get("/api/openapi.json", include_in_schema=False)
237
  async def _hub_openapi() -> JSONResponse:
 
278
 
279
  @app.get("/api/docs", include_in_schema=False)
280
  async def _hub_swagger():
281
+ return get_swagger_ui_html(
282
+ openapi_url="/api/a11oy/openapi.json",
283
+ title="a11oy API — Swagger UI",
284
+ )
285
  except Exception:
286
  # If relocation fails for any reason, fall through: the branded /docs tab
287
  # still registers below; worst case Swagger keeps /docs. Never fatal.
szl_waqay_security_loop.py ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Waqay Security Loop: a pure, proposal-only remediation control model.
2
+
3
+ This module is intentionally narrow. It does not scan source code, patch a
4
+ repository, publish an artifact, or operate a deployment. It normalizes a
5
+ small fleet catalog, derives evidence-bound findings from exact component
6
+ matches, evaluates fail-closed admission gates, and emits replayable receipts
7
+ for a bounded remediation proposal.
8
+
9
+ Truth boundary
10
+ --------------
11
+ * Every transition is ``PROPOSAL_ONLY`` and declares ``effectors = 0``.
12
+ * A content digest is tamper evidence, not a cryptographic signature.
13
+ * DSSE signing is an optional caller-provided hook. With no signer, the
14
+ envelope is explicitly ``UNSIGNED_NO_SIGNER_AVAILABLE``.
15
+ * Gate success permits only the *next proposal state*. It never authorizes an
16
+ external mutation.
17
+
18
+ The implementation is standard-library only and performs no network, process,
19
+ filesystem, registry, source-control, or deployment calls.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import base64
25
+ import hashlib
26
+ import json
27
+ import re
28
+ from dataclasses import asdict, dataclass, replace
29
+ from enum import Enum
30
+ from typing import Any, Callable, Iterable, Mapping, Sequence
31
+
32
+
33
+ SCHEMA_VERSION = "szl.waqay.security-loop.v1"
34
+ RECEIPT_PAYLOAD_TYPE = "application/vnd.szl.waqay-transition+json"
35
+ MODE = "PROPOSAL_ONLY"
36
+ EFFECTOR_COUNT = 0
37
+ MAX_FINDINGS = 128
38
+ MAX_AFFECTED_DEPLOYMENTS = 100
39
+ MAX_PLAN_BATCHES = 10
40
+ MAX_BATCH_SIZE = 25
41
+ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
42
+
43
+
44
+ class ContractError(ValueError):
45
+ """Raised when caller input violates the deterministic contract."""
46
+
47
+
48
+ class TransitionDenied(ContractError):
49
+ """Raised when a state transition or gate is fail-closed."""
50
+
51
+
52
+ class EvidenceState(str, Enum):
53
+ MEASURED = "MEASURED"
54
+ VERIFIED = "VERIFIED"
55
+ SUPPORTED = "SUPPORTED"
56
+ MODELED = "MODELED"
57
+ UNVERIFIED = "UNVERIFIED"
58
+ UNAVAILABLE = "UNAVAILABLE"
59
+ REFUTED = "REFUTED"
60
+
61
+
62
+ class LoopState(str, Enum):
63
+ DETECTED = "DETECTED"
64
+ VALIDATED = "VALIDATED"
65
+ REMEDIATION_PROPOSED = "REMEDIATION_PROPOSED"
66
+ APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
67
+ RECALL_PROPOSED = "RECALL_PROPOSED"
68
+ ROLLOFF_PROPOSED = "ROLLOFF_PROPOSED"
69
+
70
+
71
+ class PlanKind(str, Enum):
72
+ RECALL = "RECALL"
73
+ ROLLOFF = "ROLLOFF"
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class Component:
78
+ name: str
79
+ version: str
80
+ purl: str
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class Artifact:
85
+ artifact_digest: str
86
+ sbom_digest: str
87
+ provenance_digest: str
88
+ signature_state: EvidenceState
89
+ components: tuple[Component, ...]
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class Deployment:
94
+ deployment_id: str
95
+ artifact_digest: str
96
+ environment: str
97
+ owner_identity: str
98
+ rollback_digest: str
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class FleetCatalog:
103
+ observed_at: str
104
+ artifacts: tuple[Artifact, ...]
105
+ deployments: tuple[Deployment, ...]
106
+ schema_version: str = SCHEMA_VERSION
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class Advisory:
111
+ advisory_id: str
112
+ component_name: str
113
+ affected_versions: tuple[str, ...]
114
+ severity: str
115
+ evidence_digest: str
116
+ source_uri: str
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class Finding:
121
+ finding_id: str
122
+ advisory_id: str
123
+ component_name: str
124
+ component_version: str
125
+ artifact_digest: str
126
+ affected_deployments: tuple[str, ...]
127
+ advisory_evidence_digest: str
128
+ evidence_state: EvidenceState
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class GateInputs:
133
+ """Measured facts presented to the fail-closed proposal gate.
134
+
135
+ Booleans are deliberately explicit. A missing or unknown fact must be
136
+ represented as ``False`` rather than inferred from another field.
137
+ """
138
+
139
+ sbom_verified: bool
140
+ vulnerability_validated: bool
141
+ provenance_verified: bool
142
+ artifact_signature_verified: bool
143
+ principal_verified: bool
144
+ human_approval_verified: bool
145
+ rollback_target_previously_admitted: bool
146
+ graph_trust: str
147
+ unresolved_contradiction: bool
148
+ principal_id: str
149
+ approval_id: str
150
+ validation_evidence_digest: str
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class GateResult:
155
+ gate_id: str
156
+ passed: bool
157
+ evidence_state: EvidenceState
158
+ detail: str
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class RemediationPlan:
163
+ plan_id: str
164
+ kind: PlanKind
165
+ finding_id: str
166
+ target_artifact_digest: str
167
+ rollback_digest: str
168
+ deployment_ids: tuple[str, ...]
169
+ batches: tuple[tuple[str, ...], ...]
170
+ max_parallel: int
171
+ stop_on_failed_health_gate: bool
172
+ effectors: int = EFFECTOR_COUNT
173
+ mode: str = MODE
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class LoopRecord:
178
+ finding: Finding
179
+ state: LoopState
180
+ sequence: int
181
+ last_receipt_digest: str | None = None
182
+ mode: str = MODE
183
+ effectors: int = EFFECTOR_COUNT
184
+
185
+
186
+ DsseSigner = Callable[[Mapping[str, Any], str], Mapping[str, Any]]
187
+
188
+
189
+ def canonical_json(value: Any) -> bytes:
190
+ """Return deterministic UTF-8 JSON bytes for hashing and replay."""
191
+
192
+ return json.dumps(
193
+ value,
194
+ sort_keys=True,
195
+ separators=(",", ":"),
196
+ ensure_ascii=False,
197
+ allow_nan=False,
198
+ ).encode("utf-8")
199
+
200
+
201
+ def sha256_json(value: Any) -> str:
202
+ return hashlib.sha256(canonical_json(value)).hexdigest()
203
+
204
+
205
+ def _require_text(value: str, name: str) -> str:
206
+ if not isinstance(value, str) or not value.strip():
207
+ raise ContractError(f"{name} must be a non-empty string")
208
+ return value.strip()
209
+
210
+
211
+ def _require_sha256(value: str, name: str) -> str:
212
+ value = _require_text(value, name).lower()
213
+ if not _SHA256_RE.fullmatch(value):
214
+ raise ContractError(f"{name} must be a lowercase SHA-256 hex digest")
215
+ return value
216
+
217
+
218
+ def _sorted_unique(values: Iterable[str], name: str) -> tuple[str, ...]:
219
+ cleaned = tuple(sorted({_require_text(v, name) for v in values}))
220
+ if not cleaned:
221
+ raise ContractError(f"{name} must contain at least one value")
222
+ return cleaned
223
+
224
+
225
+ def normalize_catalog(catalog: FleetCatalog) -> FleetCatalog:
226
+ """Validate and canonically order a fleet catalog.
227
+
228
+ Duplicate artifact or deployment identifiers are rejected rather than
229
+ silently merged. Every deployment must reference a catalog artifact, and
230
+ every rollback digest must also be present in the catalog.
231
+ """
232
+
233
+ if catalog.schema_version != SCHEMA_VERSION:
234
+ raise ContractError(f"unsupported schema_version: {catalog.schema_version}")
235
+ _require_text(catalog.observed_at, "observed_at")
236
+
237
+ artifact_ids: set[str] = set()
238
+ artifacts: list[Artifact] = []
239
+ for artifact in catalog.artifacts:
240
+ artifact_digest = _require_sha256(artifact.artifact_digest, "artifact_digest")
241
+ if artifact_digest in artifact_ids:
242
+ raise ContractError(f"duplicate artifact_digest: {artifact_digest}")
243
+ artifact_ids.add(artifact_digest)
244
+ sbom_digest = _require_sha256(artifact.sbom_digest, "sbom_digest")
245
+ provenance_digest = _require_sha256(artifact.provenance_digest, "provenance_digest")
246
+ if not isinstance(artifact.signature_state, EvidenceState):
247
+ raise ContractError("signature_state must be an EvidenceState")
248
+ seen_components: set[tuple[str, str, str]] = set()
249
+ components: list[Component] = []
250
+ for component in artifact.components:
251
+ key = (
252
+ _require_text(component.name, "component.name"),
253
+ _require_text(component.version, "component.version"),
254
+ _require_text(component.purl, "component.purl"),
255
+ )
256
+ if key in seen_components:
257
+ raise ContractError(f"duplicate component in artifact {artifact_digest}: {key}")
258
+ seen_components.add(key)
259
+ components.append(Component(*key))
260
+ if not components:
261
+ raise ContractError(f"artifact {artifact_digest} has no SBOM components")
262
+ artifacts.append(
263
+ replace(
264
+ artifact,
265
+ artifact_digest=artifact_digest,
266
+ sbom_digest=sbom_digest,
267
+ provenance_digest=provenance_digest,
268
+ components=tuple(sorted(components, key=lambda c: (c.purl, c.version, c.name))),
269
+ )
270
+ )
271
+
272
+ if not artifacts:
273
+ raise ContractError("catalog must contain at least one artifact")
274
+
275
+ deployment_ids: set[str] = set()
276
+ deployments: list[Deployment] = []
277
+ for deployment in catalog.deployments:
278
+ deployment_id = _require_text(deployment.deployment_id, "deployment_id")
279
+ if deployment_id in deployment_ids:
280
+ raise ContractError(f"duplicate deployment_id: {deployment_id}")
281
+ deployment_ids.add(deployment_id)
282
+ artifact_digest = _require_sha256(deployment.artifact_digest, "deployment.artifact_digest")
283
+ rollback_digest = _require_sha256(deployment.rollback_digest, "deployment.rollback_digest")
284
+ if artifact_digest not in artifact_ids:
285
+ raise ContractError(f"deployment {deployment_id} references unknown artifact")
286
+ if rollback_digest not in artifact_ids:
287
+ raise ContractError(f"deployment {deployment_id} rollback digest is not admitted")
288
+ deployments.append(
289
+ replace(
290
+ deployment,
291
+ deployment_id=deployment_id,
292
+ artifact_digest=artifact_digest,
293
+ rollback_digest=rollback_digest,
294
+ environment=_require_text(deployment.environment, "environment"),
295
+ owner_identity=_require_text(deployment.owner_identity, "owner_identity"),
296
+ )
297
+ )
298
+
299
+ return FleetCatalog(
300
+ observed_at=catalog.observed_at,
301
+ artifacts=tuple(sorted(artifacts, key=lambda a: a.artifact_digest)),
302
+ deployments=tuple(sorted(deployments, key=lambda d: d.deployment_id)),
303
+ )
304
+
305
+
306
+ def catalog_payload(catalog: FleetCatalog) -> dict[str, Any]:
307
+ normalized = normalize_catalog(catalog)
308
+ return _jsonable(normalized)
309
+
310
+
311
+ def catalog_digest(catalog: FleetCatalog) -> str:
312
+ return sha256_json(catalog_payload(catalog))
313
+
314
+
315
+ def security_loop_manifest() -> dict[str, Any]:
316
+ """Return a deterministic public contract suitable for a read-only route."""
317
+
318
+ return {
319
+ "schema_version": SCHEMA_VERSION,
320
+ "mode": MODE,
321
+ "effectors": EFFECTOR_COUNT,
322
+ "external_mutations": "DISABLED",
323
+ "state_machine": [state.value for state in LoopState],
324
+ "gate_ids": [result.gate_id for result in evaluate_gates(_manifest_gate_fixture())],
325
+ "bounds": {
326
+ "max_findings": MAX_FINDINGS,
327
+ "max_affected_deployments": MAX_AFFECTED_DEPLOYMENTS,
328
+ "max_plan_batches": MAX_PLAN_BATCHES,
329
+ "max_batch_size": MAX_BATCH_SIZE,
330
+ },
331
+ "receipt": {
332
+ "content_addressed": True,
333
+ "payload_type": RECEIPT_PAYLOAD_TYPE,
334
+ "signature_default": "UNSIGNED_NO_SIGNER_AVAILABLE",
335
+ "signature_verification": "EXTERNAL_VERIFIER_REQUIRED",
336
+ },
337
+ "truth_boundary": (
338
+ "Gate success permits only the next proposal state; this module has no external effectors."
339
+ ),
340
+ }
341
+
342
+
343
+ def _manifest_gate_fixture() -> GateInputs:
344
+ """Internal false fixture used only to enumerate the stable gate contract."""
345
+
346
+ return GateInputs(
347
+ sbom_verified=False,
348
+ vulnerability_validated=False,
349
+ provenance_verified=False,
350
+ artifact_signature_verified=False,
351
+ principal_verified=False,
352
+ human_approval_verified=False,
353
+ rollback_target_previously_admitted=False,
354
+ graph_trust=EvidenceState.UNAVAILABLE.value,
355
+ unresolved_contradiction=True,
356
+ principal_id="unavailable",
357
+ approval_id="unavailable",
358
+ validation_evidence_digest="",
359
+ )
360
+
361
+
362
+ def detect_findings(
363
+ catalog: FleetCatalog,
364
+ advisories: Sequence[Advisory],
365
+ *,
366
+ max_findings: int = MAX_FINDINGS,
367
+ ) -> tuple[Finding, ...]:
368
+ """Derive deterministic findings from exact name/version matches.
369
+
370
+ This is deliberately not a semantic version-range evaluator. Advisory
371
+ adapters must normalize affected versions to explicit values first.
372
+ """
373
+
374
+ normalized = normalize_catalog(catalog)
375
+ if not 1 <= max_findings <= MAX_FINDINGS:
376
+ raise ContractError(f"max_findings must be between 1 and {MAX_FINDINGS}")
377
+
378
+ deployments_by_artifact: dict[str, tuple[str, ...]] = {}
379
+ for artifact in normalized.artifacts:
380
+ deployments_by_artifact[artifact.artifact_digest] = tuple(
381
+ d.deployment_id
382
+ for d in normalized.deployments
383
+ if d.artifact_digest == artifact.artifact_digest
384
+ )
385
+
386
+ normalized_advisories: list[Advisory] = []
387
+ advisory_ids: set[str] = set()
388
+ for advisory in advisories:
389
+ advisory_id = _require_text(advisory.advisory_id, "advisory_id")
390
+ if advisory_id in advisory_ids:
391
+ raise ContractError(f"duplicate advisory_id: {advisory_id}")
392
+ advisory_ids.add(advisory_id)
393
+ versions = _sorted_unique(advisory.affected_versions, "affected_versions")
394
+ normalized_advisories.append(
395
+ replace(
396
+ advisory,
397
+ advisory_id=advisory_id,
398
+ component_name=_require_text(advisory.component_name, "component_name"),
399
+ affected_versions=versions,
400
+ severity=_require_text(advisory.severity, "severity").upper(),
401
+ evidence_digest=_require_sha256(advisory.evidence_digest, "advisory.evidence_digest"),
402
+ source_uri=_require_text(advisory.source_uri, "source_uri"),
403
+ )
404
+ )
405
+
406
+ findings: list[Finding] = []
407
+ for advisory in sorted(normalized_advisories, key=lambda a: a.advisory_id):
408
+ versions = set(advisory.affected_versions)
409
+ for artifact in normalized.artifacts:
410
+ for component in artifact.components:
411
+ if component.name != advisory.component_name or component.version not in versions:
412
+ continue
413
+ subject = {
414
+ "advisory_id": advisory.advisory_id,
415
+ "component_name": component.name,
416
+ "component_version": component.version,
417
+ "artifact_digest": artifact.artifact_digest,
418
+ "advisory_evidence_digest": advisory.evidence_digest,
419
+ }
420
+ findings.append(
421
+ Finding(
422
+ finding_id="waqay:" + sha256_json(subject),
423
+ advisory_id=advisory.advisory_id,
424
+ component_name=component.name,
425
+ component_version=component.version,
426
+ artifact_digest=artifact.artifact_digest,
427
+ affected_deployments=deployments_by_artifact[artifact.artifact_digest],
428
+ advisory_evidence_digest=advisory.evidence_digest,
429
+ evidence_state=EvidenceState.MEASURED,
430
+ )
431
+ )
432
+ if len(findings) > max_findings:
433
+ raise ContractError("finding bound exceeded; narrow the advisory or catalog scope")
434
+ return tuple(sorted(findings, key=lambda f: f.finding_id))
435
+
436
+
437
+ def evaluate_gates(gates: GateInputs) -> tuple[GateResult, ...]:
438
+ """Evaluate independent, explicit proposal gates without aggregation loss."""
439
+
440
+ validation_digest_ok = bool(_SHA256_RE.fullmatch(gates.validation_evidence_digest.lower()))
441
+ principal_present = bool(gates.principal_id.strip())
442
+ approval_present = bool(gates.approval_id.strip())
443
+ trust_ok = gates.graph_trust in {EvidenceState.VERIFIED.value, EvidenceState.MEASURED.value}
444
+ checks = (
445
+ ("SBOM_VERIFIED", gates.sbom_verified, "SBOM digest independently verified"),
446
+ ("VULNERABILITY_VALIDATED", gates.vulnerability_validated and validation_digest_ok,
447
+ "technical witness has a valid content digest"),
448
+ ("PROVENANCE_VERIFIED", gates.provenance_verified, "build provenance verified"),
449
+ ("ARTIFACT_SIGNATURE_VERIFIED", gates.artifact_signature_verified, "artifact signature verified"),
450
+ ("PRINCIPAL_VERIFIED", gates.principal_verified and principal_present, "principal identity verified"),
451
+ ("HUMAN_APPROVAL_VERIFIED", gates.human_approval_verified and approval_present,
452
+ "human approval evidence verified"),
453
+ ("ROLLBACK_ADMITTED", gates.rollback_target_previously_admitted,
454
+ "rollback target was previously admitted"),
455
+ ("GRAPH_TRUST", trust_ok, "query-specific graph trust is measured or verified"),
456
+ ("NO_UNRESOLVED_CONTRADICTION", not gates.unresolved_contradiction,
457
+ "no unresolved contradiction affects the target"),
458
+ )
459
+ return tuple(
460
+ GateResult(
461
+ gate_id=gate_id,
462
+ passed=passed,
463
+ evidence_state=EvidenceState.VERIFIED if passed else EvidenceState.UNVERIFIED,
464
+ detail=detail if passed else f"BLOCKED: {detail}",
465
+ )
466
+ for gate_id, passed, detail in checks
467
+ )
468
+
469
+
470
+ def _all_gates_pass(results: Sequence[GateResult]) -> bool:
471
+ return bool(results) and all(result.passed for result in results)
472
+
473
+
474
+ def make_plan(
475
+ *,
476
+ kind: PlanKind,
477
+ finding: Finding,
478
+ target_artifact_digest: str,
479
+ rollback_digest: str,
480
+ deployment_ids: Sequence[str],
481
+ batch_size: int = 10,
482
+ max_parallel: int = 2,
483
+ ) -> RemediationPlan:
484
+ """Create a bounded plan description; never invoke an effector."""
485
+
486
+ target = _require_sha256(target_artifact_digest, "target_artifact_digest")
487
+ rollback = _require_sha256(rollback_digest, "rollback_digest")
488
+ deployments = _sorted_unique(deployment_ids, "deployment_ids")
489
+ if deployments != tuple(sorted(finding.affected_deployments)):
490
+ raise ContractError("plan deployment set must exactly match the finding blast radius")
491
+ if len(deployments) > MAX_AFFECTED_DEPLOYMENTS:
492
+ raise ContractError("affected deployment bound exceeded")
493
+ if not 1 <= batch_size <= MAX_BATCH_SIZE:
494
+ raise ContractError(f"batch_size must be between 1 and {MAX_BATCH_SIZE}")
495
+ if not 1 <= max_parallel <= batch_size:
496
+ raise ContractError("max_parallel must be between 1 and batch_size")
497
+ batches = tuple(
498
+ deployments[index:index + batch_size]
499
+ for index in range(0, len(deployments), batch_size)
500
+ )
501
+ if len(batches) > MAX_PLAN_BATCHES:
502
+ raise ContractError("plan batch bound exceeded")
503
+ plan_subject = {
504
+ "kind": kind.value,
505
+ "finding_id": finding.finding_id,
506
+ "target_artifact_digest": target,
507
+ "rollback_digest": rollback,
508
+ "deployment_ids": deployments,
509
+ "batches": batches,
510
+ "max_parallel": max_parallel,
511
+ "mode": MODE,
512
+ "effectors": EFFECTOR_COUNT,
513
+ }
514
+ return RemediationPlan(
515
+ plan_id="waqay-plan:" + sha256_json(plan_subject),
516
+ kind=kind,
517
+ finding_id=finding.finding_id,
518
+ target_artifact_digest=target,
519
+ rollback_digest=rollback,
520
+ deployment_ids=deployments,
521
+ batches=batches,
522
+ max_parallel=max_parallel,
523
+ stop_on_failed_health_gate=True,
524
+ )
525
+
526
+
527
+ _ALLOWED_TRANSITIONS: dict[LoopState, frozenset[LoopState]] = {
528
+ LoopState.DETECTED: frozenset({LoopState.VALIDATED}),
529
+ LoopState.VALIDATED: frozenset({LoopState.REMEDIATION_PROPOSED}),
530
+ LoopState.REMEDIATION_PROPOSED: frozenset({LoopState.APPROVAL_REQUIRED}),
531
+ LoopState.APPROVAL_REQUIRED: frozenset({LoopState.RECALL_PROPOSED}),
532
+ LoopState.RECALL_PROPOSED: frozenset({LoopState.ROLLOFF_PROPOSED}),
533
+ LoopState.ROLLOFF_PROPOSED: frozenset(),
534
+ }
535
+
536
+
537
+ def start_record(finding: Finding) -> LoopRecord:
538
+ if not finding.finding_id.startswith("waqay:"):
539
+ raise ContractError("finding_id must be content-addressed by this contract")
540
+ return LoopRecord(finding=finding, state=LoopState.DETECTED, sequence=0)
541
+
542
+
543
+ def transition(
544
+ record: LoopRecord,
545
+ next_state: LoopState,
546
+ *,
547
+ observed_at: str,
548
+ rationale: str,
549
+ gates: GateInputs | None = None,
550
+ plan: RemediationPlan | None = None,
551
+ signer: DsseSigner | None = None,
552
+ ) -> tuple[LoopRecord, dict[str, Any]]:
553
+ """Return a new immutable record and a replayable transition receipt.
554
+
555
+ Gate policy:
556
+ * ``VALIDATED`` requires reproducible vulnerability evidence.
557
+ * ``APPROVAL_REQUIRED`` records that approval is required, not granted.
558
+ * recall/rolloff proposals require every supply-chain, identity, graph, and
559
+ rollback gate, plus a matching bounded plan.
560
+ """
561
+
562
+ if next_state not in _ALLOWED_TRANSITIONS[record.state]:
563
+ raise TransitionDenied(f"transition {record.state.value} -> {next_state.value} is not allowed")
564
+ _require_text(observed_at, "observed_at")
565
+ rationale = _require_text(rationale, "rationale")
566
+ gate_results = evaluate_gates(gates) if gates is not None else tuple()
567
+
568
+ if next_state is LoopState.VALIDATED:
569
+ if gates is None:
570
+ raise TransitionDenied("VALIDATED requires gate evidence")
571
+ required = next(result for result in gate_results if result.gate_id == "VULNERABILITY_VALIDATED")
572
+ if not required.passed:
573
+ raise TransitionDenied("vulnerability validation evidence is missing or invalid")
574
+
575
+ if next_state in {LoopState.RECALL_PROPOSED, LoopState.ROLLOFF_PROPOSED}:
576
+ if gates is None or not _all_gates_pass(gate_results):
577
+ raise TransitionDenied("all safety, identity, provenance, and rollback gates must pass")
578
+ if plan is None:
579
+ raise TransitionDenied("recall/rolloff proposal requires a bounded plan")
580
+ expected_kind = PlanKind.RECALL if next_state is LoopState.RECALL_PROPOSED else PlanKind.ROLLOFF
581
+ if plan.kind is not expected_kind:
582
+ raise TransitionDenied(f"{next_state.value} requires a {expected_kind.value} plan")
583
+ if plan.finding_id != record.finding.finding_id:
584
+ raise TransitionDenied("plan finding does not match record finding")
585
+ if plan.mode != MODE or plan.effectors != EFFECTOR_COUNT:
586
+ raise TransitionDenied("plan must remain proposal-only with zero effectors")
587
+
588
+ payload: dict[str, Any] = {
589
+ "schema_version": SCHEMA_VERSION,
590
+ "mode": MODE,
591
+ "effectors": EFFECTOR_COUNT,
592
+ "sequence": record.sequence + 1,
593
+ "observed_at": observed_at,
594
+ "finding_id": record.finding.finding_id,
595
+ "from_state": record.state.value,
596
+ "to_state": next_state.value,
597
+ "rationale": rationale,
598
+ "previous_receipt_digest": record.last_receipt_digest,
599
+ "gate_results": [_jsonable(result) for result in gate_results],
600
+ "plan": _jsonable(plan) if plan is not None else None,
601
+ "truth_labels": {
602
+ "external_mutation": "NOT_PERFORMED",
603
+ "authorization_scope": "NEXT_PROPOSAL_STATE_ONLY",
604
+ "model_output": "NOT_USED_BY_CORE_STATE_MACHINE",
605
+ "signature_claim": "DETERMINED_BY_DSSE_ENVELOPE",
606
+ },
607
+ }
608
+ receipt_digest = sha256_json(payload)
609
+ envelope = _make_dsse_envelope(payload, signer)
610
+ receipt = {
611
+ "receipt_digest": receipt_digest,
612
+ "payload": payload,
613
+ "dsse_envelope": envelope,
614
+ }
615
+ updated = LoopRecord(
616
+ finding=record.finding,
617
+ state=next_state,
618
+ sequence=record.sequence + 1,
619
+ last_receipt_digest=receipt_digest,
620
+ )
621
+ return updated, receipt
622
+
623
+
624
+ def _make_dsse_envelope(payload: Mapping[str, Any], signer: DsseSigner | None) -> dict[str, Any]:
625
+ body = canonical_json(payload)
626
+ encoded = base64.b64encode(body).decode("ascii")
627
+ if signer is None:
628
+ return {
629
+ "payloadType": RECEIPT_PAYLOAD_TYPE,
630
+ "payload": encoded,
631
+ "signatures": [],
632
+ "signed": False,
633
+ "verification_state": "UNSIGNED_NO_SIGNER_AVAILABLE",
634
+ "honesty": "No signer hook supplied; no signature fabricated.",
635
+ }
636
+ envelope = dict(signer(payload, RECEIPT_PAYLOAD_TYPE))
637
+ if envelope.get("payloadType") != RECEIPT_PAYLOAD_TYPE or envelope.get("payload") != encoded:
638
+ raise ContractError("DSSE signer returned an envelope for different payload bytes")
639
+ signatures = envelope.get("signatures")
640
+ envelope["signed"] = bool(signatures)
641
+ envelope["verification_state"] = (
642
+ "SIGNED_NOT_VERIFIED_BY_THIS_MODULE" if signatures else "UNSIGNED_SIGNER_RETURNED_NO_SIGNATURE"
643
+ )
644
+ return envelope
645
+
646
+
647
+ def szl_dsse_signer_hook(payload: Mapping[str, Any], payload_type: str) -> Mapping[str, Any]:
648
+ """Optional adapter to the repository's existing DSSE implementation.
649
+
650
+ Import is delayed so the core control model remains standalone. The
651
+ existing signer already returns an honest unsigned envelope when its
652
+ runtime secret is absent.
653
+ """
654
+
655
+ from szl_dsse import sign_payload # type: ignore
656
+
657
+ return sign_payload(payload, payload_type)
658
+
659
+
660
+ def verify_receipt(receipt: Mapping[str, Any]) -> dict[str, Any]:
661
+ """Verify content address and DSSE payload binding without trusting a key."""
662
+
663
+ try:
664
+ payload = receipt["payload"]
665
+ expected = sha256_json(payload)
666
+ digest_ok = receipt.get("receipt_digest") == expected
667
+ envelope = receipt["dsse_envelope"]
668
+ payload_bytes = base64.b64decode(envelope["payload"], validate=True)
669
+ envelope_payload_ok = payload_bytes == canonical_json(payload)
670
+ payload_type_ok = envelope.get("payloadType") == RECEIPT_PAYLOAD_TYPE
671
+ truth_ok = payload.get("mode") == MODE and payload.get("effectors") == EFFECTOR_COUNT
672
+ return {
673
+ "valid": digest_ok and envelope_payload_ok and payload_type_ok and truth_ok,
674
+ "digest_ok": digest_ok,
675
+ "envelope_payload_ok": envelope_payload_ok,
676
+ "payload_type_ok": payload_type_ok,
677
+ "proposal_only_ok": truth_ok,
678
+ "signature_verified": False,
679
+ "signature_note": "Signature verification requires an independent configured verifier.",
680
+ }
681
+ except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
682
+ return {"valid": False, "reason": f"malformed receipt: {type(exc).__name__}"}
683
+
684
+
685
+ def replay_receipts(finding: Finding, receipts: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
686
+ """Replay an ordered receipt chain and reconstruct its final proposal state."""
687
+
688
+ state = LoopState.DETECTED
689
+ previous: str | None = None
690
+ expected_sequence = 1
691
+ for receipt in receipts:
692
+ verdict = verify_receipt(receipt)
693
+ if not verdict.get("valid"):
694
+ return {"valid": False, "sequence": expected_sequence, "reason": "receipt verification failed"}
695
+ payload = receipt["payload"]
696
+ try:
697
+ from_state = LoopState(payload["from_state"])
698
+ to_state = LoopState(payload["to_state"])
699
+ except (KeyError, ValueError):
700
+ return {"valid": False, "sequence": expected_sequence, "reason": "unknown transition state"}
701
+ if payload.get("sequence") != expected_sequence:
702
+ return {"valid": False, "sequence": expected_sequence, "reason": "sequence mismatch"}
703
+ if payload.get("finding_id") != finding.finding_id:
704
+ return {"valid": False, "sequence": expected_sequence, "reason": "finding mismatch"}
705
+ if payload.get("previous_receipt_digest") != previous:
706
+ return {"valid": False, "sequence": expected_sequence, "reason": "receipt chain mismatch"}
707
+ if from_state is not state or to_state not in _ALLOWED_TRANSITIONS[state]:
708
+ return {"valid": False, "sequence": expected_sequence, "reason": "state transition mismatch"}
709
+ gate_results = payload.get("gate_results")
710
+ if not isinstance(gate_results, list):
711
+ return {"valid": False, "sequence": expected_sequence, "reason": "gate results malformed"}
712
+ gate_map = {
713
+ item.get("gate_id"): item.get("passed")
714
+ for item in gate_results
715
+ if isinstance(item, Mapping)
716
+ }
717
+ if to_state is LoopState.VALIDATED and gate_map.get("VULNERABILITY_VALIDATED") is not True:
718
+ return {"valid": False, "sequence": expected_sequence, "reason": "validation gate not satisfied"}
719
+ if to_state in {LoopState.RECALL_PROPOSED, LoopState.ROLLOFF_PROPOSED}:
720
+ required_gate_ids = set(security_loop_manifest()["gate_ids"])
721
+ if set(gate_map) != required_gate_ids or not all(gate_map.values()):
722
+ return {"valid": False, "sequence": expected_sequence, "reason": "proposal gates not satisfied"}
723
+ plan = payload.get("plan")
724
+ expected_kind = "RECALL" if to_state is LoopState.RECALL_PROPOSED else "ROLLOFF"
725
+ if not isinstance(plan, Mapping) or plan.get("kind") != expected_kind:
726
+ return {"valid": False, "sequence": expected_sequence, "reason": "plan kind mismatch"}
727
+ if plan.get("finding_id") != finding.finding_id:
728
+ return {"valid": False, "sequence": expected_sequence, "reason": "plan finding mismatch"}
729
+ if plan.get("mode") != MODE or plan.get("effectors") != EFFECTOR_COUNT:
730
+ return {"valid": False, "sequence": expected_sequence, "reason": "plan truth boundary mismatch"}
731
+ state = to_state
732
+ previous = receipt["receipt_digest"]
733
+ expected_sequence += 1
734
+ return {
735
+ "valid": True,
736
+ "finding_id": finding.finding_id,
737
+ "final_state": state.value,
738
+ "receipt_count": len(receipts),
739
+ "last_receipt_digest": previous,
740
+ "mode": MODE,
741
+ "effectors": EFFECTOR_COUNT,
742
+ "verification_scope": "STRUCTURAL_CONTENT_AND_TRANSITION_ONLY",
743
+ "signature_verified": False,
744
+ }
745
+
746
+
747
+ def _jsonable(value: Any) -> Any:
748
+ if value is None:
749
+ return None
750
+ if isinstance(value, Enum):
751
+ return value.value
752
+ if hasattr(value, "__dataclass_fields__"):
753
+ return _jsonable(asdict(value))
754
+ if isinstance(value, Mapping):
755
+ return {str(key): _jsonable(item) for key, item in value.items()}
756
+ if isinstance(value, (tuple, list)):
757
+ return [_jsonable(item) for item in value]
758
+ return value
759
+
760
+
761
+ __all__ = [
762
+ "Advisory",
763
+ "Artifact",
764
+ "Component",
765
+ "ContractError",
766
+ "Deployment",
767
+ "DsseSigner",
768
+ "EFFECTOR_COUNT",
769
+ "EvidenceState",
770
+ "FleetCatalog",
771
+ "Finding",
772
+ "GateInputs",
773
+ "GateResult",
774
+ "LoopRecord",
775
+ "LoopState",
776
+ "MAX_AFFECTED_DEPLOYMENTS",
777
+ "MODE",
778
+ "PlanKind",
779
+ "RemediationPlan",
780
+ "SCHEMA_VERSION",
781
+ "TransitionDenied",
782
+ "canonical_json",
783
+ "catalog_digest",
784
+ "catalog_payload",
785
+ "detect_findings",
786
+ "evaluate_gates",
787
+ "make_plan",
788
+ "normalize_catalog",
789
+ "replay_receipts",
790
+ "security_loop_manifest",
791
+ "sha256_json",
792
+ "start_record",
793
+ "szl_dsse_signer_hook",
794
+ "transition",
795
+ "verify_receipt",
796
+ ]