aere-quantum/agents/proba-agent-ledger.mjs
Aere Network 6e62b1ad1a Add agents: a post-quantum identity, a policy and a signed action ledger for AI agents, with human approval and revocation
An agent gets an ML-DSA-65 identity and a policy (spending per time window, allowed tools and recipients, which actions need human
approval, who may revoke it). Every action it proposes is judged against the policy, signed by the agent and chained; a verifier that
does not trust the agent re-runs the policy over the whole ledger. Approvals and revocations are signed by people with their own
ML-DSA-65 keys. The ledger of an agent under a policy is one ledger: a second history is a branch, and two branches are a proof of
equivocation anyone can check with the public key alone. The README says what the verifier cannot see: entry times are bounded from
below only with a witness (anchors or a start time), and someone who sees one branch cannot know of another.

Tests: policy 23/23 with the AIP-23 reference verifier (21 run without it), ledger 51/51, approval and revocation 39/39, command line
23/23; negative control 25/25.
2026-09-29 21:59:41 +03:00

215 lines
19 KiB
JavaScript

// Proba registrului de agent (agent-ledger.mjs): identitate PQ + registru semnat, cu limita impusa pe sesiune si verificator care nu se
// increde in registru. Fiecare afirmatie are perechea ei negativa. Offline, deterministic (ceas injectat). Numai Node 24 (ML-DSA).
// Forma 2 (2026-09-29, B-17): politica primita de verificator se recalculeaza la hash, momentul intrarilor e marginit si de jos (la
// scriere de ceasul registrului; la verificare de ancore si notBefore, cand exista un martor), si fiecare atac e reprodus inainte.
import crypto from 'node:crypto';
import { definePolicy } from './agent-policy.mjs';
import { newAgentIdentity, agentIdFromKey, openLedger, resumeLedger, verifyLedger, findEquivocation, verifyEquivocation, sessionId, canonical } from './agent-ledger.mjs';
let ok = 0, rau = 0; const linii = [];
const cer = (c, ce) => { linii.push((c ? 'OK ' : 'RAU ') + ce); c ? ok++ : rau++; };
const arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
// identitate legata de cheie
const id = newAgentIdentity();
cer(id.agentId.startsWith('aere-agent:') && id.agentId === agentIdFromKey(id.publicKey), '1. agentId derivat din cheia publica');
const id2 = newAgentIdentity();
cer(id.agentId !== id2.agentId, '1. CONTROL: alta cheie -> alt agentId');
// politica: 100 pe fereastra de 3600 s, un destinatar, o unealta
const { policy, policyHash } = definePolicy({ agentId: id.agentId, spend: { amount: '100', windowSeconds: 3600 }, recipients: ['0xbbbb'], tools: ['retrieval'] });
// un ceas injectat: sesiunea incepe la t=1000
let t = 1000; const now = () => t;
const L = openLedger({ identity: id, policy, policyHash, now });
cer(L.session === sessionId(id.agentId, policyHash) && /^[0-9a-f]{32}$/.test(L.session), `1. sesiunea registrului e derivata din agent si politica (${L.session.slice(0, 8)}...)`);
cer(openLedger({ identity: id, policy, policyHash, now }).session === L.session, '1. un al doilea registru al aceluiasi agent sub aceeasi politica are ACEEASI sesiune (e o ramura, nu alt registru)');
cer(sessionId(id.agentId, definePolicy({ agentId: id.agentId, tools: ['x'] }).policyHash) !== L.session, '1. CONTROL: sub alta politica, alta sesiune');
// limita impusa PE SESIUNE: trei plati de 40 in aceeasi fereastra - a treia depaseste 100
const a = L.record({ kind: 'payment', to: '0xbbbb', amount: '40' });
const b = L.record({ kind: 'payment', to: '0xbbbb', amount: '40' });
const c = L.record({ kind: 'payment', to: '0xbbbb', amount: '40' });
cer(a.allowed && b.allowed && !c.allowed, `2. limita pe sesiune: 40+40 permise, al treilea 40 REFUZAT (${c.reason})`);
cer(/over the limit/.test(c.reason), '2. motivul refuzului e depasirea limitei');
// fereastra se roteste: dupa 3600 s cheltuiala veche nu mai conteaza
t = 1000 + 3601;
const d = L.record({ kind: 'payment', to: '0xbbbb', amount: '90' });
cer(d.allowed, '3. dupa fereastra, o plata noua de 90 e permisa (cheltuiala veche a expirat)');
// destinatar nepermis, unealta nepermisa, unealta permisa
cer(!L.record({ kind: 'payment', to: '0xcccc', amount: '1' }).allowed, '4. CONTROL: destinatar nepermis -> refuzat');
cer(!L.record({ kind: 'tool', tool: 'shell' }).allowed, '4. CONTROL: unealta nepermisa -> refuzat');
cer(L.record({ kind: 'tool', tool: 'retrieval' }).allowed, '4. unealta permisa -> allowed');
// provenienta legata (proof-of-ai)
const provenance = { model: { name: 'example-model', version: '1.0' }, prompt: 'summarize', tool: 'retrieval' };
const withProv = L.record({ kind: 'tool', tool: 'retrieval' }, { provenance });
cer(withProv.entry.body.provenance && withProv.entry.body.provenance.length === 64, '5. provenienta se leaga prin hash in intrare');
// --- verificatorul, care NU se increde in registru ---
const reg = L.export();
const v = verifyLedger(reg, { policy, policyHash, maxTime: t + 300 });
cer(v.ok && v.seq === reg.entries.length, `6. registrul intreg se verifica (${v.seq} intrari, plati permise ${v.allowedPayments}, cheltuit ${v.spent})`);
cer(v.spent === '170', `6. cheltuiala permisa re-derivata = 40+40+90 = 170 (${v.spent})`);
cer(/none/.test(v.time.lowerBound), `6. fara martor, rezultatul spune ca timpul nu e marginit de jos (${v.time.lowerBound})`);
cer(verifyLedger(reg, { policy, maxTime: t + 300 }).ok, '6. fara hash fixat, verificatorul il recalculeaza din politica si il compara cu al registrului');
// CONTROL: suma unei intrari schimbata -> semnatura pica
const t1 = JSON.parse(JSON.stringify(reg)); t1.entries[0].body.action.amount = '39';
const vt1 = verifyLedger(t1, { policy, policyHash, maxTime: t + 300 });
cer(!vt1.ok && vt1.seq === 0 && /signature/.test(vt1.error), `7. CONTROL: suma schimbata la seq 0 -> prinsa (${vt1.error})`);
// re-semnarea de catre adversar: ARE cheia agentului, deci isi poate rescrie si re-lega registrul
function resemneaza(r0, i, schimba) {
const r = JSON.parse(JSON.stringify(r0)); schimba(r);
let prev = i ? r.entries[i - 1].hash : '0'.repeat(64);
for (let k = i; k < r.entries.length; k++) {
const e = r.entries[k]; e.seq = k; e.body.seq = k; e.prev = prev;
e.signature = crypto.sign(null, Buffer.from(`${k}|${prev}|${canonical(e.body)}`, 'utf8'), id.privateKey).toString('base64');
e.hash = crypto.createHash('sha256').update(`${k}|${prev}|${canonical(e.body)}|${e.signature}`).digest('hex');
prev = e.hash;
}
return r;
}
// CONTROL: un "allowed" mincinos peste refuz, RE-SEMNAT cu cheia agentului -> verificatorul re-ruleaza politica si prinde decizia falsa
const iRef = reg.entries.findIndex((e) => e.body.action.kind === 'payment' && !e.body.decision.allowed);
const t2 = resemneaza(reg, iRef, (r) => { r.entries[iRef].body.decision = { allowed: true, reason: 'under the limit' }; });
const vt2 = verifyLedger(t2, { policy, policyHash, maxTime: t + 300 });
cer(!vt2.ok && vt2.seq === iRef && /false decision/.test(vt2.error), `8. CONTROL: "allowed" mincinos peste refuz, re-semnat corect -> prins (${vt2.error})`);
// CONTROL: o intrare stearsa -> lantul nu mai leaga
const t3 = JSON.parse(JSON.stringify(reg)); t3.entries.splice(1, 1); t3.entries.forEach((e, i) => { e.seq = i; });
const vt3 = verifyLedger(t3, { policy, policyHash, maxTime: t + 300 });
cer(!vt3.ok && /link|header/.test(vt3.error), `9. CONTROL: intrare stearsa -> prinsa (${vt3.error})`);
// CONTROL: registrul altui agent judecat cu politica noastra
const idX = newAgentIdentity();
const { policy: polX, policyHash: phX } = definePolicy({ agentId: idX.agentId, spend: { amount: '100', windowSeconds: 3600 }, recipients: ['0xbbbb'] });
const LX = openLedger({ identity: idX, policy: polX, policyHash: phX, now: () => 1000 });
LX.record({ kind: 'payment', to: '0xbbbb', amount: '10' });
const regX = LX.export();
const vX = verifyLedger(regX, { policy, policyHash, maxTime: 2000 });
cer(!vX.ok && /another policy|another agent|agentId/.test(vX.error), `10. CONTROL: registru al altui agent judecat cu politica noastra -> refuzat (${vX.error})`);
cer(verifyLedger(regX, { policy: polX, policyHash: phX, maxTime: 2000 }).ok, '10. registrul altui agent cu politica LUI se verifica (metoda e buna)');
// CONTROL: cheia publica inlocuita in registru (identitate falsa) -> agentId nu mai deriva din ea
const t4 = JSON.parse(JSON.stringify(reg)); t4.publicKeyPem = idX.publicKey.export({ type: 'spki', format: 'pem' });
const vt4 = verifyLedger(t4, { policy, policyHash, maxTime: t + 300 });
cer(!vt4.ok && /false identity/.test(vt4.error), `11. CONTROL: cheia publica inlocuita -> identitate falsa prinsa (${vt4.error})`);
// --- atacurile gasite de revizuirea adversariala (2026-09-27), fiecare reprodus inainte de reparatie ---
{
const idA = newAgentIdentity();
const { policy: pA, policyHash: phA } = definePolicy({ agentId: idA.agentId, spend: { amount: '100', windowSeconds: 3600 }, recipients: ['0xbbbb'] });
// 12: suma negativa urmata de una uriasa (inainte: amandoua allowed, verificatorul "ok")
const LA = openLedger({ identity: idA, policy: pA, policyHash: phA, now: () => 1000 });
const neg = LA.record({ kind: 'payment', to: '0xbbbb', amount: '-1000000' });
const uri = LA.record({ kind: 'payment', to: '0xbbbb', amount: '1000000' });
cer(!neg.allowed && /invalid amount/.test(neg.reason) && !uri.allowed, `12. ATAC: plata negativa REFUZATA (${neg.reason.slice(0, 60)}), iar 1.000.000 dupa ea refuzata pe limita`);
cer(!LA.record({ kind: 'payment', to: '0xbbbb', amount: '0' }).allowed && !LA.record({ kind: 'payment', to: '0xbbbb', amount: '1.5' }).allowed, '12. suma zero si suma zecimala refuzate');
cer(LA.record({ kind: 'payment', to: '0xbbbb', amount: '60' }).allowed, '12. CONTROL: o plata valida sub limita tot trece (reparatia nu blocheaza tot)');
// 13: timpul inapoi si timpul in viitorul ceasului registrului -> refuzate la scriere
cer(/backwards/.test(arunca(() => LA.record({ kind: 'payment', to: '0xbbbb', amount: '1' }, { at: 999 })) || ''), '13. ATAC: timp inapoi refuzat la scriere');
cer(/ahead of the ledger clock/.test(arunca(() => LA.record({ kind: 'payment', to: '0xbbbb', amount: '100' }, { at: 1000 + 3601 })) || ''), '13. ATAC: salt de fereastra (at = acum + 3601) refuzat la scriere');
// 14: un registru "din viitor" produs cu un ceas FALSIFICAT al agentului: se scrie, dar verificatorul, pe ceasul LUI, il refuza
let tFals = 1000; const LF = openLedger({ identity: idA, policy: pA, policyHash: phA, now: () => tFals });
let permise = 0; for (let i = 0; i < 10; i++) { tFals = 1000 + i * 3601; if (LF.record({ kind: 'payment', to: '0xbbbb', amount: '100' }).allowed) permise++; }
const vF = verifyLedger(LF.export(), { policy: pA, policyHash: phA, maxTime: 1000 + 300 });
cer(permise === 10 && !vF.ok && /future/.test(vF.error), `14. ATAC: 10 x 100 "in 10 ore" cu ceasul agentului falsificat -> verificatorul (ceasul lui) refuza la seq ${vF.seq}`);
const vF2 = verifyLedger(LF.export(), { policy: pA, policyHash: phA, maxTime: 1000 + 10 * 3601 });
cer(vF2.ok, '14. CONTROL: acelasi registru, verificat la un moment care chiar e dupa cele 10 ore, e valid (timpul real a trecut)');
}
// --- revizuirea adversariala din 2026-09-29 (B-17), fiecare atac reprodus pe forma 1 inainte de reparatie ---
{
const idB = newAgentIdentity();
const real = definePolicy({ agentId: idB.agentId, spend: { amount: '100', windowSeconds: 3600 }, recipients: ['0xbbbb'] });
const lax = definePolicy({ agentId: idB.agentId, spend: { amount: '1000000', windowSeconds: 3600 }, recipients: ['0xbbbb'] });
// 15: politica LAXA sub hash-ul celei reale (forma 1: plata 5000 permisa, verificator "ok" sub o limita reala de 100)
cer(/does not hash/.test(arunca(() => openLedger({ identity: idB, policy: lax.policy, policyHash: real.policyHash, now: () => 1000 })) || ''), '15. ATAC: registrul cinstit refuza sa se deschida cu o politica care nu da hash-ul dat');
// adversarul ocoleste biblioteca: scrie registrul sub politica laxa si ii pune in antet si in intrari hash-ul celei reale
const Llax = openLedger({ identity: idB, policy: lax.policy, now: () => 1000 });
Llax.record({ kind: 'payment', to: '0xbbbb', amount: '5000' });
// (si sesiunea, derivata din politica: adversarul o rescrie si pe ea, ca sa ajunga la judecata politicii)
const sReal = sessionId(idB.agentId, real.policyHash);
const falsificat = resemneaza(Llax.export(), 0, (r) => { r.policyHash = real.policyHash; r.session = sReal; r.entries.forEach((e) => { e.body.policyHash = real.policyHash; e.body.session = sReal; }); });
// resemneaza foloseste cheia lui id; aici adversarul e idB, deci re-semnez cu cheia lui
for (let k = 0, prev = '0'.repeat(64); k < falsificat.entries.length; k++) {
const e = falsificat.entries[k]; e.prev = prev;
e.signature = crypto.sign(null, Buffer.from(`${k}|${prev}|${canonical(e.body)}`, 'utf8'), idB.privateKey).toString('base64');
e.hash = crypto.createHash('sha256').update(`${k}|${prev}|${canonical(e.body)}|${e.signature}`).digest('hex'); prev = e.hash;
}
const vLax = verifyLedger(falsificat, { policy: lax.policy, policyHash: real.policyHash, maxTime: 2000 });
cer(!vLax.ok && /does not hash to the pinned policyHash/.test(vLax.error), `15. ATAC: politica laxa data verificatorului cu hash-ul celei reale -> refuzata (${vLax.error})`);
const vReal = verifyLedger(falsificat, { policy: real.policy, policyHash: real.policyHash, maxTime: 2000 });
cer(!vReal.ok && /false decision/.test(vReal.error), '15. cu politica reala, plata de 5000 e prinsa ca decizie falsa (limita 100)');
// 16: antedatare (forma 1: 10 plati de 100 facute in aceeasi secunda, declarate in 10 ferestre trecute, verificator "ok")
const acum = 100000;
const Lb = openLedger({ identity: idB, policy: real.policy, now: () => acum });
cer(/behind the ledger clock/.test(arunca(() => Lb.record({ kind: 'payment', to: '0xbbbb', amount: '100' }, { at: acum - 36010 })) || ''), '16. ATAC: registrul cinstit refuza la scriere o intrare antedatata cu peste 300 s');
cer(Lb.record({ kind: 'payment', to: '0xbbbb', amount: '100' }, { at: acum - 200 }).allowed, '16. CONTROL: o intrare in toleranta (200 s in urma) se scrie');
// adversarul ocoleste biblioteca: un ceas mincinos care merge in trecut pe masura ce scrie
let tb = acum - 36010; const Lant = openLedger({ identity: idB, policy: real.policy, now: () => tb });
let permise = 0; for (let i = 0; i < 10; i++) { tb = acum - 36010 + i * 3601; if (Lant.record({ kind: 'payment', to: '0xbbbb', amount: '100' }).allowed) permise++; }
const antedatat = Lant.export();
const vFaraMartor = verifyLedger(antedatat, { policy: real.policy, maxTime: acum + 300 });
cer(permise === 10 && vFaraMartor.ok && /none/.test(vFaraMartor.time.lowerBound), '16. fara martor, antedatarea NU se poate vedea, si rezultatul o spune (lowerBound: none)');
// martorul: capul registrului vazut la 'acum - 3600' (de pilda notarizat), deci intrarile de dupa el nu pot declara mai devreme
const cap = { seq: 4, hash: antedatat.entries[4].hash, at: acum - 3600 };
const vAncorat = verifyLedger(antedatat, { policy: real.policy, maxTime: acum + 300, anchors: [cap] });
cer(!vAncorat.ok && vAncorat.seq === 5 && /backdated/.test(vAncorat.error), `16. ATAC: cu un cap ancorat la un martor, prima intrare antedatata de dupa el e prinsa (seq ${vAncorat.seq})`);
const vStart = verifyLedger(antedatat, { policy: real.policy, maxTime: acum + 300, notBefore: acum - 7200 });
cer(!vStart.ok && vStart.seq === 0 && /witnessed start/.test(vStart.error), '16. cu inceputul sesiunii vazut de martor (notBefore), intrarile de dinainte sunt prinse');
// controlul pozitiv al ancorelor: un registru cinstit, ancorat la fiecare 3 intrari cu momentul scrierii, trece
let tc = acum; const Lc = openLedger({ identity: idB, policy: real.policy, now: () => tc });
const ancore = [];
for (let i = 0; i < 6; i++) { tc = acum + i * 1200; const r = Lc.record({ kind: 'payment', to: '0xbbbb', amount: '10' }); if (i % 3 === 2) ancore.push({ seq: r.entry.seq, hash: r.entry.hash, at: tc + 5 }); }
cer(verifyLedger(Lc.export(), { policy: real.policy, maxTime: tc + 300, anchors: ancore, notBefore: acum }).ok, '16. CONTROL: un registru cinstit, ancorat, trece cu martorii lui');
// o ancora a altei ramuri (acelasi agent, alta istorie) nu se potriveste
const alta = openLedger({ identity: idB, policy: real.policy, now: () => acum }); alta.record({ kind: 'payment', to: '0xbbbb', amount: '11' });
const vRamura = verifyLedger(Lc.export(), { policy: real.policy, maxTime: tc + 300, anchors: [{ seq: 0, hash: alta.export().entries[0].hash, at: acum }] });
cer(!vRamura.ok && /another branch/.test(vRamura.error), '16. CONTROL: o ancora a altei istorii nu se potriveste cu registrul (alta ramura sau alt registru)');
// 17: intrarea poarta sesiunea registrului; o intrare mutata din alt registru nu se potriveste
const s2 = resemneaza(reg, 0, (r) => { r.entries[0].body.session = 'f'.repeat(32); });
const vS = verifyLedger(s2, { policy, policyHash, maxTime: t + 300 });
cer(!vS.ok && vS.seq === 0 && /session/.test(vS.error), `17. CONTROL: o intrare care numeste alta sesiune decat registrul -> prinsa (${vS.error})`);
const s3 = resemneaza(reg, 0, (r) => { r.session = 'f'.repeat(32); r.entries.forEach((e) => { e.body.session = 'f'.repeat(32); }); });
const vS3 = verifyLedger(s3, { policy, policyHash, maxTime: t + 300 });
cer(!vS3.ok && /derived/.test(vS3.error), `17. ATAC: un registru cu o sesiune aleasa de agent (nu cea derivata), re-semnat -> refuzat (${vS3.error})`);
// 18: cheltuiala dubla prin "al doilea registru" = o ramura; doua intrari semnate de agent la aceeasi pozitie sunt dovada
const idC = newAgentIdentity();
const pc = definePolicy({ agentId: idC.agentId, spend: { amount: '100', windowSeconds: 3600 }, recipients: ['0xbbbb'] });
const R1 = openLedger({ identity: idC, policy: pc.policy, now: () => 5000 });
const R2 = openLedger({ identity: idC, policy: pc.policy, now: () => 5000 });
const d1 = R1.record({ kind: 'payment', to: '0xbbbb', amount: '100' }), d2 = R2.record({ kind: 'payment', to: '0xbbbb', amount: '99' });
const v1 = verifyLedger(R1.export(), { policy: pc.policy, maxTime: 5300 }), v2 = verifyLedger(R2.export(), { policy: pc.policy, maxTime: 5300 });
cer(d1.allowed && d2.allowed && v1.ok && v2.ok, '18. ATAC: doua registre ale aceluiasi agent, 100 + 99 sub o limita de 100; fiecare, SINGUR, verifica (cine vede o ramura nu o vede pe cealalta)');
const dov = findEquivocation(R1.export(), R2.export());
cer(!!dov && dov.seq === 0 && verifyEquivocation(dov).ok, '18. dar impreuna sunt o dovada de echivocare, verificabila de oricine numai cu cheia publica');
const dovRau = JSON.parse(JSON.stringify(dov)); dovRau.b.body.action.amount = '98';
cer(!verifyEquivocation(dovRau).ok, '18. CONTROL: o "dovada" cu o intrare nesemnata de agent (continut schimbat) e respinsa');
const dovX = { ...JSON.parse(JSON.stringify(dov)), publicKeyPem: idX.publicKey.export({ type: 'spki', format: 'pem' }) };
cer(!verifyEquivocation(dovX).ok, '18. CONTROL: o "dovada" care pune alta cheie publica e respinsa (agentId nu deriva din ea)');
cer(findEquivocation(R1.export(), R1.export()) === null, '18. CONTROL: acelasi registru cu el insusi nu e echivocare');
const R1b = resumeLedger({ identity: idC, policy: pc.policy, ledger: R1.export(), now: () => 5010 });
R1b.record({ kind: 'tool', tool: 'x' });
cer(findEquivocation(R1.export(), R1b.export()) === null, '18. CONTROL: un registru si continuarea lui (prefix comun) nu sunt echivocare');
// 19: repornirea: resumeLedger continua starea (cheltuiala din fereastra, nonce-urile), si refuza un registru care nu verifica
cer(!R1b.record({ kind: 'payment', to: '0xbbbb', amount: '1' }).allowed, '19. dupa repornire, cheltuiala din fereastra e pastrata: 100 deja cheltuit, inca 1 -> refuzat');
const R1c = resumeLedger({ identity: idC, policy: pc.policy, ledger: R1b.export(), now: () => 5000 + 3601 });
cer(R1c.record({ kind: 'payment', to: '0xbbbb', amount: '100' }).allowed && verifyLedger(R1c.export(), { policy: pc.policy, maxTime: 9000 }).ok, '19. dupa fereastra, registrul reluat permite iar si ramane verificabil de la capat');
const stricat = JSON.parse(JSON.stringify(R1b.export())); stricat.entries[0].body.action.amount = '1';
cer(/does not verify/.test(arunca(() => resumeLedger({ identity: idC, policy: pc.policy, ledger: stricat, now: () => 5010 })) || ''), '19. CONTROL: un registru atins nu se poate relua');
cer(/another agent/.test(arunca(() => resumeLedger({ identity: idC, policy: pc.policy, ledger: regX, now: () => 5010 })) || ''), '19. CONTROL: registrul altui agent nu se poate relua');
}
for (const l of linii) console.log(l);
console.log(`agent-ledger: ${ok}/${ok + rau} cum trebuia`);
process.exitCode = rau ? 1 : 0;