Spaces:
Running
Running
chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)
Browse filesAutomated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, 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 +13 -1
- a11oy_ayllu.py +249 -37
- ayllu/backend.py +57 -10
- ayllu/loop.py +11 -0
- ayllu/lounge.py +18 -14
- research/__init__.py +3 -0
- research/a11oy_primary_project_registry.py +448 -0
- serve.py +173 -51
- szl3d_holographic.py +1 -0
- szl_be_hardening.py +54 -31
- szl_claim_rupture_gate.py +476 -0
- szl_hub.py +9 -7
- szl_waqay_security_loop.py +796 -0
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/
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 132 |
-
#
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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){
|
| 538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,
|
| 540 |
c=>({'&':'&','<':'<','>':'>'}[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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
+`
|
| 611 |
-
+`
|
| 612 |
+`<br>Nemo: ${esc((route.experts_selected||[]).join(' + ')||route.state||'unavailable')}`
|
| 613 |
-
+`
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 829 |
-
|
| 830 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
|
|
|
|
|
|
| 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 & 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=>({'&':'&','<':'<','>':'>'}[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.
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 +
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
| 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
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
except Exception as exc:
|
| 112 |
return {
|
| 113 |
-
"text":
|
| 114 |
"model": "error",
|
| 115 |
"stub": True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
if not isinstance(result, dict):
|
| 118 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 43 |
-
|
| 44 |
-
|
| 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 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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=
|
| 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 |
-
|
| 2890 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 3874 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 3884 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 4923 |
-
#
|
| 4924 |
-
#
|
| 4925 |
-
#
|
| 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 |
-
|
| 5133 |
-
|
|
|
|
| 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 |
-
|
| 5229 |
-
|
|
|
|
| 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 |
-
|
| 5239 |
-
|
|
|
|
| 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 |
-
|
| 5249 |
-
|
|
|
|
| 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": "
|
| 13292 |
"path": "",
|
| 13293 |
"relation": "declared-source-with-hf-overlay",
|
| 13294 |
"state": "VERIFIED_REFERENCE",
|
| 13295 |
-
"evidence_url": "https://github.com/szl-holdings/a11oy/commit/
|
| 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 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 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"
|
| 317 |
-
|
| 318 |
-
|
| 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
|
| 324 |
]
|
| 325 |
return list(self._mem)
|
| 326 |
|
| 327 |
def count(self) -> int:
|
| 328 |
with self._lock:
|
| 329 |
-
if self.backend == "sqlite"
|
| 330 |
-
|
|
|
|
| 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 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
"
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 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
|
| 221 |
-
|
| 222 |
-
|
| 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"
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
+
]
|