// AERE Agent Ledger (roadmap B2 / #35-37, seam-ul care lipsea): identitatea post-cuantica a unui agent AI + registrul lui de actiuni, // impus CRIPTOGRAFIC si verificabil de oricine, fara incredere in agent. // // agent-policy.mjs are checkAction PUR (nu tine stare, primeste cheltuiala din fereastra). O limita de cheltuiala "pe fereastra" nu // inseamna nimic fara CINEVA care aduna cheltuiala reala si o impune la fiecare actiune - altfel agentul poate spune de fiecare data // "n-am cheltuit nimic". Aici e acel cineva: un registru per identitate care // 1. deriva cheltuiala din fereastra din PROPRIILE lui intrari permise (starea, nu vorba agentului), // 2. cheama checkAction cu ea, deci limita e impusa PE TOATA SESIUNEA, // 3. semneaza fiecare intrare cu cheia ML-DSA-65 a agentului si o leaga intr-un lant sha256(seq|prev|corp|semnatura), // 4. leaga, optional, provenienta iesirii (model/versiune/prompt/unealta, digestul unui plic proof-of-ai). // Identitatea agentului e LEGATA de cheie: agentId = 'aere-agent:' + primii 20 de octeti din sha256(SPKI-ul cheii publice). Nu poti // pretinde alt agentId cu cheia ta, si nu poti semna in numele altui agent fara cheia lui. // // VERIFICATORUL (verifyLedger) NU se increde in ce scrie registrul despre sine: recalculeaza hash-ul politicii primite, reface lantul // de hash-uri, RE-VERIFICA fiecare semnatura fata de cheia publica a agentului, si RE-RULEAZA checkAction de la inceput ca sa confirme // ca decizia scrisa (allowed/refuz) e chiar cea pe care o da politica, si ca nicio cheltuiala permisa nu a depasit vreodata limita. O // intrare cu suma schimbata, semnata de alta cheie, stearsa, sau cu un "allowed" mincinos peste un refuz, e prinsa. // // TIMPUL, spus exact (2026-09-29, B-17): momentul unei intrari e declaratia celui care tine cheia agentului. Verificatorul il margineste // de SUS cu ceasul lui (sau cu momentul unui cap ancorat); de JOS numai daca primeste ancore de la un martor (`anchors`: capete ale // registrului cu momentul la care martorul le-a vazut) sau un `notBefore`. Fara ele, cine tine cheia poate ANTEDATA intrari si imparti // o cheltuiala peste ferestre trecute (masurat pe forma 1: 10 plati de 100 facute in aceeasi secunda, declarate in 10 ferestre, sub o // limita de 100 pe ora, verificator "ok"); cu ancore, antedatarea e marginita la intervalul dintre doua ancore. Registrul cinstit // (biblioteca de aici) refuza la scriere un moment mai departe de ceasul lui decat TOLERANTA_S, in ambele sensuri. // // UN SINGUR REGISTRU pe agent si politica (2026-09-29, B-17): limita "pe fereastra" era impusa pe REGISTRU, iar un registru nou se // deschidea oricand cu o sesiune aleatoare, deci cine tine cheia putea deschide N registre si cheltui de N ori limita, fiecare // verificat "ok" (masurat pe forma 1 prin aceeasi aprobare pentru 5000 folosita in doua registre). Acum sesiunea e DERIVATA din agent // si politica (`sessionId`), deci un al doilea registru nu e "alt registru", ci o RAMURA a aceluiasi: doua intrari semnate de agent cu // aceeasi sesiune si acelasi seq si continut diferit sunt o dovada de echivocare pe care o verifica oricine (`findEquivocation`, // `verifyEquivocation`), fara incredere in nimeni. Un agent care reporneste isi continua registrul (`resumeLedger`); unul care si-a // pierdut registrul nu il poate relua fara sa para o ramura, si asta e pretul cerut: registrul se tine durabil (de pilda in jurnalul // stratului de verificare), sau se trece la o politica noua. Ce NU se poate: cine vede o singura ramura nu stie ca exista alta; // o vede un martor al capului (`anchors`) sau cine primeste ambele. // // Numai Node 24 (crypto.sign/verify cu ML-DSA, null ca algoritm). Nu atinge reteaua, nu tine bani reali: `amount` e o unitate abstracta // a politicii (wei AERE sau orice altceva ce numeste politica); mutarea reala a valorii ramane a stratului de plata (x402), care poate // cere acest registru drept dovada ca actiunea a fost permisa de politica agentului. import crypto from 'node:crypto'; import { checkAction, hashPolicy, canonical } from './agent-policy.mjs'; import { verifyApproval, verifyRevocation, validRevocations } from './agent-aprobare.mjs'; export { canonical }; export const VERSION = 'aere-agent-ledger/2 (2026-09-29)'; const ALG = 'ml-dsa-65'; const sha = (s) => crypto.createHash('sha256').update(typeof s === 'string' ? Buffer.from(s, 'utf8') : s).digest('hex'); const GEN = '0'.repeat(64); const TOLERANTA_S = 300; // cat se poate departa momentul unei intrari de ceasul care o judeca (registrul la scriere, verificatorul la citire) // 2026-09-28 (punctul 22): aprobarea umana si revocarea. O intrare poate purta aprobarile care i-au fost date (`body.approvals`, numai // cand exista); registrul le verifica, numara aprobatorii distincti VALIZI si ii da lui checkAction. Un nonce de aprobare se poate // folosi o singura data pe tot lantul: a doua aparitie nu mai numara. Revocarea semnata de proprietar opreste tot de la momentul ei; // registrul o scrie ca intrare (`body.revocation`), iar verificatorul o primeste si SEPARAT, fiindca un agent isi poate omite revocarea // din propriul registru. function aprobatoriValizi(aprobari, { policy, policyHash, action, at, folosite }) { const valizi = [], respinse = []; for (const ap of aprobari || []) { if (ap && folosite.has(String(ap.nonce))) { respinse.push('nonce already used'); continue; } const r = verifyApproval(ap, { policy, policyHash, action, at }); if (r.ok) valizi.push(r.humanId); else respinse.push(r.error); } return { valizi, respinse }; } const nonceuri = (aprobari) => (aprobari || []).filter((a) => a && a.nonce != null).map((a) => String(a.nonce)); /** SPKI-ul (DER) al unei chei publice, ca punct unic de adevar al identitatii. */ function spkiDer(publicKey) { return publicKey.export({ type: 'spki', format: 'der' }); } /** agentId derivat din cheia publica: nefalsificabil fara cheie. */ export function agentIdFromKey(publicKey) { return 'aere-agent:' + sha(spkiDer(publicKey)).slice(0, 40); } /** * O identitate noua de agent (cheie ML-DSA-65 + agentId legat de ea). * @returns {{agentId:string, publicKey:crypto.KeyObject, privateKey:crypto.KeyObject, publicKeyPem:string}} */ export function newAgentIdentity() { const { publicKey, privateKey } = crypto.generateKeyPairSync(ALG); return { agentId: agentIdFromKey(publicKey), publicKey, privateKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }) }; } /** Identitatea unui agent din cheia lui privata (PEM PKCS#8). */ export function agentFromPrivateKeyPem(pem) { const privateKey = crypto.createPrivateKey(pem); if (privateKey.asymmetricKeyType !== ALG) throw new Error('agent-ledger: the key is not ML-DSA-65'); const publicKey = crypto.createPublicKey(privateKey); return { agentId: agentIdFromKey(publicKey), publicKey, privateKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }) }; } export function publicKeyFromPem(pem) { return crypto.createPublicKey(pem); } /** Sesiunea registrului unui agent sub o politica: derivata, deci una singura. */ export function sessionId(agentId, policyHash) { return sha(`aere-agent-ledger-session|${agentId}|${String(policyHash).toLowerCase()}`).slice(0, 32); } const ceasulLocal = () => Math.floor(Date.now() / 1000); function pregateste({ identity, policy: politicaData, policyHash: hashDat }) { if (!identity || !identity.privateKey || !identity.publicKey) throw new Error('agent-ledger: an identity with privateKey and publicKey is required'); if (identity.agentId !== agentIdFromKey(identity.publicKey)) throw new Error('agent-ledger: agentId is not derived from the identity key'); const { policy, policyHash } = hashPolicy(politicaData); if (hashDat != null && String(hashDat).toLowerCase() !== policyHash) throw new Error('agent-ledger: the policy does not hash to the policyHash given'); if (policy.agentId !== identity.agentId) throw new Error('agent-ledger: the policy belongs to another agent'); return { policy, policyHash }; } /** * Deschide registrul NOU al unei identitati sub o politica (seq 0). `now` e injectabil (fereastra deterministica in probe). Politica se * normalizeaza si hash-ul ei se RECALCULEAZA: o pereche politica+hash care nu se potriveste e refuzata aici, nu doar la verificare. * A doua deschidere sub aceeasi politica e o RAMURA a primului registru (aceeasi sesiune): pentru o repornire se foloseste resumeLedger. * @param {object} p {identity:{agentId,privateKey,publicKey}, policy, policyHash?, now?} */ export function openLedger({ identity, policy, policyHash, now = ceasulLocal }) { return registru(identity, pregateste({ identity, policy, policyHash }), now, [], null); } /** * Continua un registru exportat (repornirea agentului). Il verifica intai ca un strain (verifyLedger, pe ceasul registrului); refuza * un registru care nu verifica sau al altei identitati, si reface starea: intrarile, nonce-urile folosite, revocarea. * @param {object} p {identity, policy, policyHash?, ledger: exportul, now?, revocations?} */ export function resumeLedger({ identity, policy, policyHash, ledger, now = ceasulLocal, revocations = [] }) { const pol = pregateste({ identity, policy, policyHash }); if (!ledger || ledger.agentId !== identity.agentId) throw new Error('agent-ledger: the ledger to resume belongs to another agent'); const v = verifyLedger(ledger, { policy: pol.policy, policyHash: pol.policyHash, maxTime: Number(now()) + TOLERANTA_S, revocations }); if (!v.ok) throw new Error(`agent-ledger: the ledger to resume does not verify${v.seq != null ? ` at seq ${v.seq}` : ''}: ${v.error}`); return registru(identity, pol, now, JSON.parse(JSON.stringify(ledger.entries)), v.revocations.revokedAt); } function registru(identity, { policy, policyHash }, now, entries, revocatInitial) { const session = sessionId(identity.agentId, policyHash); const folosite = new Set(); // nonce-urile aprobarilor deja purtate de intrari for (const e of entries) for (const n of nonceuri(e.body.approvals)) folosite.add(n); let revokedAt = revocatInitial; // cea mai timpurie revocare valida primita // cheltuielile PERMISE, cu momentul lor, pentru fereastra (numai platile allowed; checkAction filtreaza singur dupa fereastra) function spentInWindow() { return entries.filter((e) => e.body.decision.allowed && e.body.action.kind === 'payment') .map((e) => ({ amount: String(e.body.action.amount), at: e.body.action.at })); } function scrie(corpFaraSeq) { const seq = entries.length; const prev = seq ? entries[seq - 1].hash : GEN; const body = { ...corpFaraSeq, seq }; const mesaj = Buffer.from(`${seq}|${prev}|${canonical(body)}`, 'utf8'); const signature = crypto.sign(null, mesaj, identity.privateKey).toString('base64'); const hash = sha(`${seq}|${prev}|${canonical(body)}|${signature}`); const entry = { seq, prev, body, signature, hash }; entries.push(entry); return entry; } function momentul(opt) { const at = opt.at ?? now(); if (!Number.isFinite(Number(at))) throw new Error('agent-ledger: the entry time is not a finite number'); if (entries.length && Number(at) < Number(entries[entries.length - 1].body.at)) throw new Error('agent-ledger: entry times cannot go backwards'); const acum = Number(now()); if (Number(at) > acum + TOLERANTA_S) throw new Error(`agent-ledger: the entry time is more than ${TOLERANTA_S} s ahead of the ledger clock`); // 2026-09-29 (B-17): si in urma; o intrare antedatata muta o plata intr-o fereastra trecuta if (Number(at) < acum - TOLERANTA_S) throw new Error(`agent-ledger: the entry time is more than ${TOLERANTA_S} s behind the ledger clock (backdated)`); return Number(at); } const antet = { v: 2, agentId: identity.agentId, policyHash, session }; return { agentId: identity.agentId, session, policyHash, /** * Revocarea agentului, semnata de proprietarul numit in politica. Se scrie ca intrare; de la `revokedAt` totul e refuzat. * O revocare care nu verifica (alt semnatar, alta politica) e refuzata cu motivul, si nu se scrie. */ recordRevocation(rv, opt = {}) { const r = verifyRevocation(rv, { policy, policyHash }); if (!r.ok) throw new Error(`agent-ledger: revocation refused: ${r.error}`); const at = momentul(opt); revokedAt = revokedAt == null ? r.revokedAt : Math.min(revokedAt, r.revokedAt); const action = { kind: 'revocation', revokedAt: r.revokedAt, at }; return scrie({ ...antet, at, action, decision: { allowed: false, reason: `revocation recorded (from ${r.revokedAt})` }, provenance: null, revocation: rv }); }, /** * Propune o actiune. Cheama checkAction cu cheltuiala reala din fereastra; scrie o intrare semnata si legata. * @param {object} actionIn {kind:'payment'|'tool', amount?, to?, asset?, tool?, args?} * @param {object} [opt] {provenance?: plic proof-of-ai / obiect (se leaga prin hash), at?: number, approvals?: aprobari umane} * @returns {{allowed:boolean, reason:string, entry:object, rejectedApprovals:string[]}} */ record(actionIn, opt = {}) { // 2026-09-27 (revizuire adversariala): `at` ales de apelant reseta fereastra la vointa. Timpul unei intrari e NEDESCRESCATOR si // nu se poate departa de ceasul registrului cu mai mult de TOLERANTA_S; verificatorul cere acelasi lucru fata de ceasul LUI. const at = momentul(opt); // checkAction citeste "acum" din action.at si filtreaza fereastra dupa el: injectam at in actiune, si o stocam asa cum a fost judecata const action = { ...actionIn, at }; const approvals = Array.isArray(opt.approvals) && opt.approvals.length ? opt.approvals : null; const { valizi, respinse } = aprobatoriValizi(approvals, { policy, policyHash, action, at, folosite }); const decizie = checkAction(policy, action, spentInWindow(), { approvers: valizi, revokedAt }); for (const n of nonceuri(approvals)) folosite.add(n); const body = { ...antet, at, action, decision: { allowed: !!decizie.allowed, reason: decizie.reason }, provenance: opt.provenance ? sha(canonical(opt.provenance)) : null }; if (approvals) body.approvals = approvals; const entry = scrie(body); return { allowed: entry.body.decision.allowed, reason: entry.body.decision.reason, entry, rejectedApprovals: respinse }; }, /** Registrul de export: identitate (cheie publica), politica (prin hash), sesiunea si intrarile. */ export() { return { version: VERSION, agentId: identity.agentId, publicKeyPem: identity.publicKey.export({ type: 'spki', format: 'pem' }), policyHash, session, entries: entries.slice() }; }, entries() { return entries.slice(); }, }; } /** * Verifica un registru exportat FARA sa se increada in el: politica primita (recalculata la hash), identitatea legata de cheie, fiecare * semnatura, lantul de hash-uri, si - decisiv - RE-RULEAZA politica de la inceput ca sa confirme ca fiecare decizie scrisa e cea corecta * si ca nicio cheltuiala permisa nu a depasit limita. * @param {object} ledger exportul unui registru * @param {object} o * @param {object} o.policy politica (de la oricine: hash-ul ei se recalculeaza si trebuie sa fie al registrului) * @param {string} [o.policyHash] hash-ul fixat de cel care verifica (de ex. dintr-o autorizare); daca lipseste, cel al politicii * @param {number} [o.maxTime] marginea de SUS a timpului (secunde unix); implicit ceasul local + TOLERANTA_S * @param {Array} [o.revocations] revocarile primite de la proprietar, nu din registru * @param {Array} [o.anchors] [{seq, hash, at}]: capete ale registrului vazute de un martor la momentul `at` * @param {number} [o.notBefore] marginea de JOS a timpului, de la un martor (de ex. deschiderea sesiunii) * @param {number} [o.anchorTolerance] cat poate intarzia martorul fata de scriere (implicit TOLERANTA_S) * @returns {{ok:boolean, seq?:number, error?:string, allowedPayments?:number, spent?:string, humanApproved?:number, revocations:object, time:object}} */ export function verifyLedger(ledger, { policy: politicaData, policyHash: hashFixat, maxTime = Math.floor(Date.now() / 1000) + TOLERANTA_S, revocations = [], anchors = [], notBefore = null, anchorTolerance = TOLERANTA_S } = {}) { const timp = { maxTime: Number(maxTime), notBefore: notBefore == null ? null : Number(notBefore), anchors: (anchors || []).length, lowerBound: notBefore != null || (anchors || []).length ? 'witnessed' : 'none: entry times are the statement of the agent key holder' }; const rezultat = (x, extra = {}) => ({ ...x, ...extra, time: timp }); if (!ledger || !Array.isArray(ledger.entries)) return rezultat({ ok: false, error: 'a ledger with entries is required' }); let policy, policyHash; try { ({ policy, policyHash } = hashPolicy(politicaData)); } catch (e) { return rezultat({ ok: false, error: `the policy given is not valid: ${e.message}` }); } if (hashFixat != null && String(hashFixat).toLowerCase() !== policyHash) return rezultat({ ok: false, error: 'the policy given does not hash to the pinned policyHash (a different policy)' }); const ext = validRevocations(revocations, { policy, policyHash }); let revokedAt = ext.revokedAt; const judecat = () => ({ revocations: { given: (revocations || []).length, rejected: ext.rejected.length, revokedAt, setHash: ext.setHash } }); if (String(ledger.policyHash).toLowerCase() !== policyHash) return rezultat({ ok: false, error: 'the ledger names another policy (policyHash differs)' }, judecat()); if (ledger.session !== sessionId(ledger.agentId, policyHash)) return rezultat({ ok: false, error: 'the ledger session is not the one derived from the agent and the policy' }, judecat()); let pub; try { pub = crypto.createPublicKey(ledger.publicKeyPem); } catch { return rezultat({ ok: false, error: 'the public key cannot be read' }, judecat()); } if (ledger.agentId !== agentIdFromKey(pub)) return rezultat({ ok: false, error: 'agentId is not derived from the public key (false identity)' }, judecat()); if (policy.agentId !== ledger.agentId) return rezultat({ ok: false, error: 'the policy belongs to another agent' }, judecat()); // ancorele: fiecare numeste o intrare a ACESTUI registru (altfel e alta ramura sau alt registru) const anc = []; for (const a of anchors || []) { const s = Number(a && a.seq); if (!Number.isInteger(s) || s < 0 || s >= ledger.entries.length) return rezultat({ ok: false, error: `anchor at seq ${a && a.seq} is outside the ledger` }, judecat()); if (!ledger.entries[s] || ledger.entries[s].hash !== a.hash) return rezultat({ ok: false, seq: s, error: `anchor at seq ${s} does not match the ledger (another branch or another ledger)` }, judecat()); if (!Number.isFinite(Number(a.at))) return rezultat({ ok: false, error: `anchor at seq ${s} has no time` }, judecat()); anc.push({ seq: s, at: Number(a.at) }); } const tol = Number(anchorTolerance); let prev = GEN, humanApproved = 0; const folosite = new Set(); const permise = []; // cheltuielile permise re-derivate, pentru fereastra for (let i = 0; i < ledger.entries.length; i++) { const e = ledger.entries[i]; const esec = (error) => rezultat({ ok: false, seq: i, error }, judecat()); if (e.seq !== i) return esec(`seq ${e.seq} instead of ${i}`); if (e.prev !== prev) return esec('prev does not link to the previous hash (an entry was removed or reordered)'); const b = e.body; if (!b || b.v !== 2 || b.seq !== i || b.agentId !== ledger.agentId || b.session !== ledger.session) return esec('the entry body does not match its header (agent, session or seq)'); if (String(b.policyHash).toLowerCase() !== policyHash) return esec('the entry names another policy'); const mesaj = Buffer.from(`${i}|${prev}|${canonical(b)}`, 'utf8'); let sigOk = false; try { sigOk = crypto.verify(null, mesaj, pub, Buffer.from(String(e.signature), 'base64')); } catch { sigOk = false; } if (!sigOk) return esec('the signature does not verify (content changed or another key)'); const hash = sha(`${i}|${prev}|${canonical(b)}|${e.signature}`); if (hash !== e.hash) return esec('the entry hash does not reproduce'); // timpul: nedescrescator, sub marginea de sus, peste marginea de jos si intre ancorele care il incadreaza const atI = Number(b.at); if (!Number.isFinite(atI) || !b.action || Number(b.action.at) !== atI) return esec('the entry time is missing or differs from the time judged'); if (i > 0 && atI < Number(ledger.entries[i - 1].body.at)) return esec('entry times go backwards'); if (atI > Number(maxTime)) return esec(`entry from the future: ${atI} > ${maxTime} (the verifier clock or an anchored head)`); if (notBefore != null && atI < Number(notBefore)) return esec(`entry declares ${atI}, before the witnessed start ${notBefore}`); for (const a of anc) { if (i <= a.seq && atI > a.at + tol) return esec(`entry declares ${atI}, after the anchor at seq ${a.seq} that covers it was witnessed (${a.at})`); if (i > a.seq && atI < a.at - tol) return esec(`entry declares ${atI}, before the anchor at seq ${a.seq} that precedes it was witnessed (${a.at}): backdated`); } // o intrare de revocare: revocarea din ea trebuie sa verifice, si ea nu permite nimic if (b.revocation !== undefined) { const r = verifyRevocation(b.revocation, { policy, policyHash }); if (!r.ok) return esec(`the revocation entry carries a revocation that does not verify (${r.error})`); if (b.action.kind !== 'revocation' || Number(b.action.revokedAt) !== r.revokedAt || b.decision.allowed) return esec('the revocation entry does not say what it carries'); revokedAt = revokedAt == null ? r.revokedAt : Math.min(revokedAt, r.revokedAt); prev = hash; continue; } if (b.action.kind === 'revocation') return esec('a revocation entry without the revocation'); if (b.approvals !== undefined && !(Array.isArray(b.approvals) && b.approvals.length)) return esec('the approvals field is empty or not a list'); const { valizi } = aprobatoriValizi(b.approvals, { policy, policyHash, action: b.action, at: atI, folosite }); for (const n of nonceuri(b.approvals)) folosite.add(n); const decizieReala = checkAction(policy, b.action, permise, { approvers: valizi, revokedAt }); if (!b.decision || !!decizieReala.allowed !== !!b.decision.allowed) { return esec(`false decision: the ledger says allowed=${b.decision && b.decision.allowed}, the policy gives allowed=${decizieReala.allowed} (${decizieReala.reason})`); } if (decizieReala.allowed && decizieReala.approvedBy) humanApproved++; if (decizieReala.allowed && b.action.kind === 'payment') permise.push({ amount: String(b.action.amount), at: b.action.at }); prev = hash; } const spent = permise.reduce((a, s) => a + BigInt(s.amount), 0n); return rezultat({ ok: true, seq: ledger.entries.length, allowedPayments: permise.length, spent: String(spent), humanApproved }, judecat()); } // DOVADA DE ECHIVOCARE: doua intrari semnate de aceeasi cheie de agent, cu aceeasi sesiune si acelasi seq, si continut diferit. Cum // sesiunea e una pe agent si politica, asta inseamna ca agentul a scris doua istorii diferite ale aceluiasi registru (o aprobare // folosita de doua ori, o cheltuiala dubla). Dovada se verifica fara incredere in cine o aduce: numai cheia publica si doua semnaturi. function intrareSemnata(e, pub) { if (!e || !e.body || !Number.isInteger(e.seq) || e.body.seq !== e.seq) return false; let ok = false; try { ok = crypto.verify(null, Buffer.from(`${e.seq}|${e.prev}|${canonical(e.body)}`, 'utf8'), pub, Buffer.from(String(e.signature), 'base64')); } catch { ok = false; } return ok && sha(`${e.seq}|${e.prev}|${canonical(e.body)}|${e.signature}`) === e.hash; } /** * Cauta, in doua exporturi ale aceluiasi agent, prima pozitie la care ele difera; daca ambele intrari de acolo sunt semnate de agent in * aceeasi sesiune, intoarce dovada. @returns {object|null} */ export function findEquivocation(a, b) { if (!a || !b || a.agentId !== b.agentId || a.session !== b.session) return null; let pub; try { pub = crypto.createPublicKey(a.publicKeyPem); } catch { return null; } if (agentIdFromKey(pub) !== a.agentId) return null; const n = Math.min((a.entries || []).length, (b.entries || []).length); for (let i = 0; i < n; i++) { const x = a.entries[i], y = b.entries[i]; if (x.hash === y.hash) continue; if (x.body.session !== a.session || y.body.session !== a.session || !intrareSemnata(x, pub) || !intrareSemnata(y, pub)) return null; return { v: 1, kind: 'aere-agent-equivocation', agentId: a.agentId, session: a.session, publicKeyPem: a.publicKeyPem, seq: i, a: x, b: y }; } return null; } /** @returns {{ok:boolean, error?:string}} */ export function verifyEquivocation(p) { if (!p || p.kind !== 'aere-agent-equivocation' || p.v !== 1) return { ok: false, error: 'not an aere-agent-equivocation v1' }; let pub; try { pub = crypto.createPublicKey(p.publicKeyPem); } catch { return { ok: false, error: 'the public key cannot be read' }; } if (agentIdFromKey(pub) !== p.agentId) return { ok: false, error: 'agentId is not derived from the public key' }; for (const e of [p.a, p.b]) { if (!e || !e.body || e.seq !== p.seq || e.body.agentId !== p.agentId || e.body.session !== p.session) return { ok: false, error: 'the two entries are not at the same position of the same ledger' }; if (!intrareSemnata(e, pub)) return { ok: false, error: 'an entry is not signed by the agent' }; } if (p.a.body.policyHash !== p.b.body.policyHash || p.session !== sessionId(p.agentId, p.a.body.policyHash)) return { ok: false, error: 'the session is not the one derived from the agent and the policy' }; if (p.a.hash === p.b.hash) return { ok: false, error: 'the two entries are the same' }; return { ok: true }; }