Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 106 additions & 23 deletions site/proofs.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,113 @@

.skip-link{position:fixed;left:12px;top:12px;z-index:1000;transform:translateY(-160%);background:#fff;color:#172019;padding:10px 12px;border:2px solid currentColor;border-radius:6px;font:600 12px Inter,ui-sans-serif,system-ui,sans-serif}.skip-link:focus{transform:translateY(0)}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{transition:none!important;animation:none!important}}
</style></head><body><a class="skip-link" href="#proof-main">Skip to main content</a><div class="wrap"><nav><a href="/">agentscope /</a><a href="/demo.html">Demo ↗</a></nav><main id="proof-main" tabindex="-1"><p class="mono">Habitat · portable evidence</p><h1>Proof snapshots.</h1><p class="lede">Historical Habitat proof exports copied into AgentScope. Each bundle is checked in your browser for its embedded SHA-256 content digest and claim/action consistency.</p><div class="notice"><strong>Important:</strong> a matching content hash detects changes to the exported bundle, but does not authenticate who published it. These are historical records, not live status. An unsigned proof does not independently establish that the claimed work happened in the outside world.</div><div id="proof-list" class="list" aria-live="polite"><div class="empty">Loading proof index…</div></div></main><footer>AgentScope · Evidence is labelled by what can actually be checked. <a href="/">Back to AgentScope</a></footer></div>
</style></head><body><a class="skip-link" href="#proof-main">Skip to main content</a><div class="wrap"><nav><a href="/">agentscope /</a><a href="/demo.html">Demo ↗</a></nav><main id="proof-main" tabindex="-1"><p class="mono">Habitat · portable evidence</p><h1>Proof snapshots.</h1><p class="lede">Historical Habitat proof exports copied into AgentScope. Each bundle is checked in your browser for structure, its SHA-256 digest, a separately pinned manifest digest and claim/action consistency.</p><div class="notice"><strong>Important:</strong> a matching content hash detects changes to the exported bundle, but does not authenticate who published it. These are historical records, not live status. An unsigned proof does not independently establish that the claimed work happened in the outside world. The ledger integrity value is the result recorded by Habitat at export time; this page does not recompute the underlying SQLite action hash chain.</div><div id="proof-list" class="list" aria-live="polite"><div class="empty">Loading proof index…</div></div></main><footer>AgentScope · Evidence is labelled by what can actually be checked. <a href="/">Back to AgentScope</a></footer></div>
<script>
function canonical(v){if(Array.isArray(v))return "["+v.map(canonical).join(",")+"]";if(v&&typeof v==="object")return "{"+Object.keys(v).sort().map(k=>JSON.stringify(k)+":"+canonical(v[k])).join(",")+"}";return JSON.stringify(v)}
async function verify(bundle,expectedDigest){
const errors=[];
if(!bundle||typeof bundle!=="object"||Array.isArray(bundle))return{ok:false,errors:["Proof bundle is not an object"],signature:"absent"};
for(const key of ["proof_version","generated_at","claim","ledger","content_sha256"])if(!(key in bundle))errors.push("Missing "+key);
function isObject(value){return value!==null&&typeof value==="object"&&!Array.isArray(value)}
function isDateTime(value){return typeof value==="string"&&/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value)&&!Number.isNaN(Date.parse(value))}
function finiteNumbers(value){if(typeof value==="number")return Number.isFinite(value);if(Array.isArray(value))return value.every(finiteNumbers);if(isObject(value))return Object.values(value).every(finiteNumbers);return true}
function validateStructure(bundle,errors){
const allowed=["proof_version","generated_at","claim","ledger","content_sha256","signature"];
const required=["proof_version","generated_at","claim","ledger","content_sha256"];
const unknown=Object.keys(bundle).filter(key=>!allowed.includes(key)).sort();
const missing=required.filter(key=>!(key in bundle));
if(unknown.length)errors.push("Unknown fields: "+unknown.join(", "));
if(missing.length)errors.push("Missing fields: "+missing.join(", "));
if(!finiteNumbers(bundle))errors.push("Non-finite JSON numbers are not allowed");
if(bundle.proof_version!=="1")errors.push("Unsupported proof version");
if(!bundle.claim||typeof bundle.claim!=="object")errors.push("Claim object missing");
if(!bundle.ledger||!Array.isArray(bundle.ledger.actions))errors.push("Ledger actions missing");
if(bundle.ledger&&bundle.ledger.integrity!=="intact")errors.push("Ledger integrity is not intact");
if(errors.some(x=>x.startsWith("Missing ")||x==="Unsupported proof version"||x==="Claim object missing"||x==="Ledger actions missing"))return{ok:false,errors,signature:bundle.signature?"present":"absent"};
const input={...bundle};delete input.generated_at;delete input.content_sha256;delete input.signature;
const digest=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(input))))].map(x=>x.toString(16).padStart(2,"0")).join("");
if(digest!==bundle.content_sha256)errors.push("Embedded content hash mismatch");
if(!expectedDigest||digest!==expectedDigest)errors.push("Manifest-pinned content hash mismatch");
const claim=bundle.claim,evidence=claim.evidence||{};
if(claim.status==="verified"){
if(!evidence.action_id)errors.push("Verified claim lacks evidence action ID");
const action=bundle.ledger.actions.find(x=>x.id===evidence.action_id);
if(!action)errors.push("Evidence action absent from ledger");
else if(action.status!==claim.expected_status||action.action!==claim.action)errors.push("Evidence action does not match claim");
if(!isDateTime(bundle.generated_at))errors.push("generated_at must be an ISO-8601 date-time with timezone");
if(typeof bundle.content_sha256!=="string"||!/^[0-9a-f]{64}$/.test(bundle.content_sha256))errors.push("content_sha256 must be 64 lowercase hexadecimal characters");
const claim=bundle.claim,ledger=bundle.ledger;
const claimFields=["id","habitat_id","job_id","claim","action","expected_status","created_at","verified_at","status","evidence","run_id"];
if(!isObject(claim))errors.push("claim must be an object");
else{
if(Object.keys(claim).some(key=>!claimFields.includes(key)))errors.push("claim contains unknown fields");
if(claimFields.some(key=>!(key in claim)))errors.push("claim is missing required fields");
for(const key of ["id","habitat_id","claim","expected_status","status"])if(typeof claim[key]!=="string")errors.push("claim."+key+" must be a string");
if(!isObject(claim.evidence))errors.push("claim.evidence must be an object");
for(const key of ["job_id","action","run_id"])if(claim[key]!==null&&claim[key]!==undefined&&typeof claim[key]!=="string")errors.push("claim."+key+" must be a string or null");
if(!isDateTime(claim.created_at))errors.push("claim.created_at must be an ISO-8601 date-time with timezone");
if(claim.verified_at!==null&&!isDateTime(claim.verified_at))errors.push("claim.verified_at must be an ISO-8601 date-time with timezone or null");
}
if(!isObject(ledger))errors.push("ledger must be an object");
else{
if(Object.keys(ledger).sort().join(",")!=="actions,integrity")errors.push("ledger must contain exactly integrity and actions");
if(!["intact","failed"].includes(ledger.integrity))errors.push("ledger.integrity must be intact or failed");
if(!Array.isArray(ledger.actions))errors.push("ledger.actions must be an array");
else{
if(ledger.actions.length>10000)errors.push("ledger.actions exceeds maximum of 10000");
const actionFields=["id","habitat_id","job_id","timestamp","actor","action","status","details","run_id"];
ledger.actions.slice(0,10000).forEach((action,index)=>{
if(!isObject(action)){errors.push("ledger.actions["+index+"] must be an object");return}
if(Object.keys(action).sort().join(",")!==actionFields.slice().sort().join(","))errors.push("ledger.actions["+index+"] has invalid fields");
for(const key of ["id","habitat_id","actor","action","status"])if(typeof action[key]!=="string")errors.push("ledger.actions["+index+"]."+key+" must be a string");
if(!isObject(action.details))errors.push("ledger.actions["+index+"].details must be an object");
for(const key of ["job_id","run_id"])if(action[key]!==null&&action[key]!==undefined&&typeof action[key]!=="string")errors.push("ledger.actions["+index+"]."+key+" must be a string or null");
if(!isDateTime(action.timestamp))errors.push("ledger.actions["+index+"].timestamp must be an ISO-8601 date-time with timezone");
});
}
}
if("signature" in bundle){
const signature=bundle.signature;
if(!isObject(signature))errors.push("signature must be an object");
else{
const fields=["algorithm","key_id","agent_id","public_key","signature"];
if(Object.keys(signature).sort().join(",")!==fields.slice().sort().join(","))errors.push("signature has invalid fields");
if(signature.algorithm!=="Ed25519")errors.push("signature.algorithm must be Ed25519");
for(const key of ["key_id","agent_id"])if(signature[key]!==null&&signature[key]!==undefined&&typeof signature[key]!=="string")errors.push("signature."+key+" must be a string or null");
for(const key of ["public_key","signature"])if(typeof signature[key]!=="string"||!signature[key])errors.push("signature."+key+" must be a non-empty string");
}
}
}
async function verify(bundle,manifestDigest){
const errors=[];
if(!isObject(bundle))return{ok:false,errors:["Proof bundle must be a JSON object"],signature:"absent"};
const structural=[];
validateStructure(bundle,structural);errors.push(...structural);
let actual=null;
if(typeof bundle.content_sha256==="string"){
try{
const input={...bundle};delete input.generated_at;delete input.content_sha256;delete input.signature;
actual=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(input))))].map(x=>x.toString(16).padStart(2,"0")).join("");
}catch(error){errors.push("Content hash computation failed");}
if(actual!==null&&actual!==bundle.content_sha256)errors.push("Embedded content hash mismatch");
}
if(!manifestDigest||actual!==manifestDigest)errors.push("Manifest-pinned content hash mismatch");
const beforeRelationships=errors.length;
const claim=bundle.claim,ledger=bundle.ledger;
if(isObject(claim)&&isObject(ledger)&&Array.isArray(ledger.actions)){
for(const [index,action] of ledger.actions.entries()){
if(!isObject(action))continue;
if(action.habitat_id!==claim.habitat_id)errors.push("ledger.actions["+index+"] habitat_id does not match claim");
if(claim.job_id!==null&&claim.job_id!==undefined&&action.job_id!==claim.job_id)errors.push("ledger.actions["+index+"] job_id does not match claim");
if(claim.run_id!==null&&claim.run_id!==undefined&&action.run_id!==claim.run_id)errors.push("ledger.actions["+index+"] run_id does not match claim");
}
const evidence=claim.evidence;
if(isObject(evidence)){
if(claim.status==="verified"){
if(!Object.keys(evidence).length)errors.push("verified claim must include evidence");
else{
if(typeof evidence.source!=="string"||!evidence.source)errors.push("verified claim evidence must identify a source");
if(evidence.status!==claim.expected_status)errors.push("verified claim evidence status does not match claim.expected_status");
if(evidence.source==="habitat_trusted_ledger"&&!evidence.action_id)errors.push("trusted-ledger verification must identify evidence.action_id");
}
}
if(evidence.action_id!==undefined&&evidence.action_id!==null){
const matched=ledger.actions.find(action=>isObject(action)&&action.id===evidence.action_id);
if(!matched)errors.push("claim evidence action_id is absent from ledger.actions");
else{
if(claim.action!==null&&claim.action!==undefined&&matched.action!==claim.action)errors.push("claim evidence action does not match claim.action");
if(claim.expected_status!==null&&claim.expected_status!==undefined&&matched.status!==claim.expected_status)errors.push("claim evidence action status does not match claim.expected_status");
if(claim.run_id!==null&&claim.run_id!==undefined&&matched.run_id!==claim.run_id)errors.push("claim evidence action run_id does not match claim.run_id");
}
}
}
}
return{ok:errors.length===0,errors,digest,signature:bundle.signature?"present":"absent"};
if(claim&&claim.status==="verified"&&(!ledger||ledger.integrity!=="intact"))errors.push("verified claim cannot have failed ledger integrity");
const relationshipsValid=errors.length===beforeRelationships;
const contentValid=typeof actual==="string"&&actual===bundle.content_sha256;
return{ok:structural.length===0&&contentValid&&relationshipsValid&&actual===manifestDigest,errors,digest:actual,signature:bundle.signature?"present":"absent",ledgerIntegrity:ledger&&ledger.integrity||"unknown"};
}
function el(t,c,s){const n=document.createElement(t);if(c)n.className=c;if(s!==undefined)n.textContent=s;return n}
async function main(){
Expand All @@ -77,13 +160,13 @@
if(proofUrl.origin!==location.origin)throw Error("Proof URL must remain on this site");
const proofResponse=await fetch(proofUrl.href,{cache:"no-store"});
if(!proofResponse.ok)throw Error("Could not load proof ("+proofResponse.status+")");
const bundle=await proofResponse.json();
const raw=await proofResponse.text();\n if(new TextEncoder().encode(raw).length>16*1024*1024)throw Error("Proof bundle exceeds 16 MiB");\n const bundle=JSON.parse(raw);
const result=await verify(bundle,item.content_sha256);
card.append(el("p","claim",bundle.claim&&bundle.claim.claim?bundle.claim.claim:"Claim text unavailable"));
const checks=el("div","checks");
checks.append(
el("span","check "+(result.ok?"ok":"bad"),result.ok?"PROOF VERIFIED":"PROOF CHECK FAILED"),
el("span","check "+(bundle.ledger&&bundle.ledger.integrity==="intact"?"ok":"bad"),"LEDGER: "+String(bundle.ledger&&bundle.ledger.integrity||"unknown").toUpperCase()),
el("span","check "+(result.ok?"ok":"bad"),result.ok?"BUNDLE CHECKS PASSED":"BUNDLE CHECK FAILED"),
el("span","check "+(bundle.ledger&&bundle.ledger.integrity==="intact"?"ok":"bad"),"LEDGER EXPORT: "+String(bundle.ledger&&bundle.ledger.integrity||"unknown").toUpperCase()),
el("span","check warn","SIGNATURE: "+(result.signature==="present"?"PRESENT · UNVERIFIED":"ABSENT")),
el("span","check","CLAIM: "+String(bundle.claim&&bundle.claim.status||"unknown").toUpperCase())
);
Expand Down
70 changes: 69 additions & 1 deletion test_generate.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import sqlite3
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
Expand Down Expand Up @@ -227,13 +229,79 @@ def test_demo_mobile_tables_keep_cadence_and_action_status_visible(self):
def test_proof_explorer_checks_manifest_pinned_digest_and_handles_bad_ledger(self):
root = Path(__file__).resolve().parent
proofs = (root / "site" / "proofs.html").read_text(encoding="utf-8")
self.assertIn("async function verify(bundle,expectedDigest)", proofs)
self.assertIn("async function verify(bundle,manifestDigest)", proofs)
self.assertIn("Manifest-pinned content hash mismatch", proofs)
self.assertIn("Embedded content hash mismatch", proofs)
self.assertIn('bundle.ledger&&bundle.ledger.integrity', proofs)
self.assertIn("Proof URL must remain on this site", proofs)
self.assertIn("Proof could not be verified:", proofs)


def test_browser_proof_assurance_matches_habitat_verifier_boundaries(self):
root = Path(__file__).resolve().parent
proofs = (root / "site" / "proofs.html").read_text(encoding="utf-8")
self.assertIn("function validateStructure(bundle,errors)", proofs)
self.assertIn("claim evidence status does not match claim.expected_status", proofs)
self.assertIn("ledger.actions[", proofs)
self.assertIn("MAX_PROOF_ACTIONS", proofs) if "MAX_PROOF_ACTIONS" in proofs else self.assertIn("ledger.actions exceeds maximum of 10000", proofs)
self.assertIn("LEDGER EXPORT:", proofs)
self.assertIn("does not recompute the underlying SQLite action hash chain", proofs)
self.assertIn("new TextEncoder().encode(raw).length>16*1024*1024", proofs)


def test_browser_proof_verifier_executes_against_habitat_export_fixtures(self):
node = shutil.which("node")
if not node:
self.skipTest("Node.js is not installed")
root = Path(__file__).resolve().parent
page = (root / "site" / "proofs.html").read_text(encoding="utf-8")
script = page.split("<script>", 1)[1].split("</script>", 1)[0]
verifier = script[:script.index("function el(")]
runtime_test = r"""
const {webcrypto}=require("crypto");
globalThis.crypto=webcrypto;
const fs=require("fs");
const manifest=JSON.parse(fs.readFileSync("site/proofs-manifest.json","utf8"));
const fixtures=[
["Greenlight-K53","greenlight_k53-9dc5a00-0"],
["Diketo","diketo-30feea5-0"]
];
async function digest(bundle){
const input={...bundle};
delete input.generated_at;delete input.content_sha256;delete input.signature;
return [...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(input))))].map(x=>x.toString(16).padStart(2,"0")).join("");
}
(async()=>{
for(const [project,run] of fixtures){
const entry=manifest.projects[project].find(x=>x.run_id===run);
const bundle=JSON.parse(fs.readFileSync("site/proofs/"+project+"/"+run+"/proof.json","utf8"));
const valid=await verify(bundle,entry.content_sha256);
if(!valid.ok)throw new Error(project+" fixture rejected: "+valid.errors.join("; "));
const wrongPin=await verify(bundle,"0".repeat(64));
if(wrongPin.ok||!wrongPin.errors.some(x=>x.includes("Manifest-pinned")))throw new Error("wrong manifest digest was accepted");
const tampered=JSON.parse(JSON.stringify(bundle));tampered.claim.claim+=" tampered";
const changed=await verify(tampered,entry.content_sha256);
if(changed.ok)throw new Error("tampered proof was accepted");
const broken=JSON.parse(JSON.stringify(bundle));delete broken.ledger.actions;
const malformed=await verify(broken,entry.content_sha256);
if(malformed.ok)throw new Error("malformed ledger was accepted");
const crossRun=JSON.parse(JSON.stringify(bundle));crossRun.ledger.actions[0].run_id="other-run";crossRun.content_sha256=await digest(crossRun);
const relationship=await verify(crossRun,crossRun.content_sha256);
if(relationship.ok||!relationship.errors.some(x=>x.includes("run_id does not match claim")))throw new Error("cross-run action was accepted");
}
console.log("Browser proof verifier: 2 Habitat fixtures valid; bad pin, tampering, malformed ledger and cross-run action rejected.");
})().catch(error=>{console.error(error);process.exit(1)});
"""
result = subprocess.run(
[node, "-e", verifier + "\n" + runtime_test],
cwd=root,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("2 Habitat fixtures valid", result.stdout)


if __name__ == "__main__":
unittest.main()
Loading