aere-quantum/agents/proba-agent-aprobare.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

148 lines
13 KiB
JavaScript

// Proba aprobarii umane si a revocarii (agent-aprobare.mjs + agent-ledger.mjs + agent-policy.mjs, punctul 22). Offline, ceas injectat,
// chei ML-DSA-65 reale. Fiecare afirmatie are perechea ei negativa; adversarul are cheia AGENTULUI (isi poate re-semna registrul),
// dar nu cheile oamenilor. Forma 2 (2026-09-29, B-17): aprobarea leaga argumentele uneltei; un al doilea registru e o ramura.
// node proba-agent-aprobare.mjs iesire 0 = toate cum trebuia
import crypto from 'node:crypto';
import { definePolicy, checkAction } from './agent-policy.mjs';
import { newAgentIdentity, openLedger, resumeLedger, verifyLedger, findEquivocation, verifyEquivocation, canonical } from './agent-ledger.mjs';
import { newHumanIdentity, approve, revoke, verifyApproval } from './agent-aprobare.mjs';
let ok = 0, rau = 0;
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
const arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
const agent = newAgentIdentity();
const [H1, H2, H3, O, X] = [newHumanIdentity(), newHumanIdentity(), newHumanIdentity(), newHumanIdentity(), newHumanIdentity()];
const { policy, policyHash } = definePolicy({
agentId: agent.agentId, spend: { amount: '5000', windowSeconds: 3600 }, recipients: ['0xbbbb'], tools: ['retrieval', 'deploy'],
approval: { approvers: [H1.humanId, H2.humanId, H3.humanId], threshold: 2, above: '100', tools: ['deploy'] }, owner: O.humanId,
});
let t = 1000; const now = () => t;
const L = openLedger({ identity: agent, policy, policyHash, now });
const ap = (h, action, o = {}) => approve({ human: h, agentId: agent.agentId, policyHash, action,
issuedAt: o.issuedAt ?? t - 10, expiresAt: o.expiresAt ?? t + 600, nonce: o.nonce });
const P500 = { kind: 'payment', to: '0xbbbb', amount: '500' };
// 1. politica: validarea campurilor noi, si politicile vechi raman neatinse
cer(!('approval' in definePolicy({ agentId: agent.agentId }).policy) && !('owner' in definePolicy({ agentId: agent.agentId }).policy), '1. o politica fara aprobare nu poarta campurile noi (hash-ul politicilor vechi neschimbat)');
cer(!!arunca(() => definePolicy({ agentId: 'a', approval: { approvers: [H1.humanId], threshold: 2, above: '1' } })), '1. CONTROL: prag peste numarul aprobatorilor -> refuzat');
cer(!!arunca(() => definePolicy({ agentId: 'a', approval: { approvers: ['0xabc'], threshold: 1, above: '1' } })), '1. CONTROL: un aprobator care nu e id aere-human -> refuzat');
cer(!!arunca(() => definePolicy({ agentId: 'a', approval: { approvers: [H1.humanId], threshold: 1 } })), '1. CONTROL: aprobare fara `above` si fara `tools` (nu ar cere nimic) -> refuzata');
// 2. pragul de aprobare
cer(L.record({ kind: 'payment', to: '0xbbbb', amount: '50' }).allowed, '2. plata de 50 (sub prag) trece fara aprobare');
const r0 = L.record(P500);
cer(!r0.allowed && /0 of 2/.test(r0.reason), `2. CONTROL: 500 fara aprobare -> refuzat (${r0.reason})`);
const r1 = L.record(P500, { approvals: [ap(H1, P500)] });
cer(!r1.allowed && /1 of 2/.test(r1.reason), `2. CONTROL: 500 cu o singura aprobare -> refuzat (${r1.reason})`);
const aprobariBune = [ap(H1, P500), ap(H2, P500)];
const r2 = L.record(P500, { approvals: aprobariBune });
cer(r2.allowed && /approved by 2 of 2/.test(r2.reason), `2. 500 cu doua aprobari de la oameni distincti -> permis (${r2.reason})`);
// 3. ce NU numara ca aprobare
const rX = L.record(P500, { approvals: [ap(H1, P500), ap(X, P500)] });
cer(!rX.allowed && rX.rejectedApprovals.length === 1 && /not named in the policy/.test(rX.rejectedApprovals[0]), '3. CONTROL: a doua aprobare de la un om nenumit in politica -> respinsa cu motivul ei, actiunea refuzata');
// al doilea strat al aceluiasi paznic, probat separat (altfel scoaterea unuia nu se vede, fiindca il prinde celalalt)
cer(/not named/.test(verifyApproval(ap(X, P500), { policy, policyHash, action: P500, at: t }).error || ''), '3. CONTROL, stratul aprobarii: aprobarea unui om nenumit nu verifica');
cer(!checkAction(policy, { ...P500, at: t }, [], { approvers: [H1.humanId, X.humanId] }).allowed, '3. CONTROL, stratul politicii: un id nenumit dat lui checkAction nu numara');
cer(!L.record(P500, { approvals: [ap(H1, P500), ap(H1, P500)] }).allowed, '3. CONTROL: acelasi om de doua ori (nonce-uri diferite) -> refuzat (distincti)');
const P600 = { kind: 'payment', to: '0xbbbb', amount: '600' };
const r3 = L.record(P600, { approvals: [ap(H1, P500), ap(H2, P500)] });
cer(!r3.allowed && r3.rejectedApprovals.length === 2 && r3.rejectedApprovals.every((m) => /another action/.test(m)), '3. CONTROL: aprobarile pentru 500 folosite la 600 -> respinse (alta actiune)');
const r4 = L.record(P500, { approvals: aprobariBune });
cer(!r4.allowed && r4.rejectedApprovals.length === 2 && r4.rejectedApprovals.every((m) => /nonce already used/.test(m)), '3. CONTROL: aceleasi aprobari (aceleasi nonce-uri) a doua oara -> respinse (nonce folosit)');
const r5 = L.record(P500, { approvals: [ap(H1, P500, { issuedAt: t - 900, expiresAt: t - 1 }), ap(H2, P500)] });
cer(!r5.allowed && r5.rejectedApprovals.some((m) => /outside the approval window/.test(m)), '3. CONTROL: o aprobare expirata nu numara');
const clasic = crypto.generateKeyPairSync('ed25519');
const falsa = { ...ap(H1, P500), approverPem: clasic.publicKey.export({ type: 'spki', format: 'pem' }) };
cer(/ML-DSA-65/.test(verifyApproval(falsa, { policy, policyHash, action: P500, at: t }).error || ''), '3. CONTROL: o aprobare cu cheie clasica (ed25519) e respinsa');
const PMARE = { kind: 'payment', to: '0xbbbb', amount: '6000' };
const r6 = L.record(PMARE, { approvals: [ap(H1, PMARE), ap(H3, PMARE)] });
cer(!r6.allowed && /over the limit/.test(r6.reason), `3. aprobarea NU scuteste de limita: 6000 cu doua aprobari -> refuzat (${r6.reason})`);
// 3b. B-17 (2026-09-29). Forma 1: o singura aprobare pentru 5000 a trecut in doua registre ale aceluiasi agent, fiecare verificat "ok".
// Acum al doilea registru sub aceeasi politica e o RAMURA a primului (aceeasi sesiune): cine vede o singura ramura o accepta, dar
// cele doua impreuna sunt o dovada de echivocare semnata de agent.
const L2 = openLedger({ identity: agent, policy, policyHash, now });
const P700 = { kind: 'payment', to: '0xbbbb', amount: '700' };
const pentruL = [ap(H1, P700), ap(H2, P700)];
cer(L.record(P700, { approvals: pentruL }).allowed, '3b. doua aprobari pentru 700 in registrul L -> permis');
const reluat = L2.record(P700, { approvals: pentruL });
cer(reluat.allowed && verifyLedger(L2.export(), { policy, policyHash, maxTime: t + 300 }).ok, '3b. ATAC: aceleasi aprobari reluate intr-un "al doilea registru" trec acolo, si ramura, SINGURA, verifica');
const dov = findEquivocation(L.export(), L2.export());
cer(!!dov && dov.seq === 0 && verifyEquivocation(dov).ok, '3b. dar L si L2 sunt aceeasi sesiune: impreuna dau o dovada de echivocare verificabila de oricine');
// 4. unelte care cer aprobare, cu argumentele legate
const DEP = { kind: 'tool', tool: 'deploy' };
cer(!L.record(DEP).allowed, '4. CONTROL: unealta "deploy" fara aprobare -> refuzata');
cer(L.record(DEP, { approvals: [ap(H2, DEP), ap(H3, DEP)] }).allowed, '4. "deploy" cu doua aprobari -> permisa');
cer(L.record({ kind: 'tool', tool: 'retrieval' }).allowed, '4. "retrieval" (necerand aprobare) trece fara');
const STAGING = { kind: 'tool', tool: 'deploy', args: { target: 'staging' } };
const PROD = { kind: 'tool', tool: 'deploy', args: { target: 'production' } };
const rProd = L.record(PROD, { approvals: [ap(H1, STAGING), ap(H2, STAGING)] });
cer(!rProd.allowed && rProd.rejectedApprovals.every((m) => /another action/.test(m)), '4. ATAC (B-17): aprobarile unui deploy pe staging folosite pe production -> respinse (argumentele difera)');
cer(L.record(STAGING, { approvals: [ap(H1, STAGING), ap(H3, STAGING)] }).allowed, '4. CONTROL: aprobarile pentru staging trec pe staging');
// 5. verificatorul re-verifica aprobarile; adversarul are cheia agentului si isi poate re-semna registrul
const exp = L.export();
const v = verifyLedger(exp, { policy, policyHash, maxTime: t + 300 });
cer(v.ok && v.humanApproved === 4, `5. registrul cinstit verifica; ${v.humanApproved} actiuni aprobate de oameni`);
function resemneaza(reg, i, schimba) {
const r = JSON.parse(JSON.stringify(reg));
schimba(r.entries[i].body);
for (let k = i; k < r.entries.length; k++) {
const e = r.entries[k]; e.prev = k ? r.entries[k - 1].hash : '0'.repeat(64);
e.signature = crypto.sign(null, Buffer.from(`${k}|${e.prev}|${canonical(e.body)}`, 'utf8'), agent.privateKey).toString('base64');
e.hash = crypto.createHash('sha256').update(`${k}|${e.prev}|${canonical(e.body)}|${e.signature}`).digest('hex');
}
return r;
}
const iAprobat = exp.entries.findIndex((e) => e.body.decision.allowed && e.body.approvals && e.body.action.kind === 'payment');
const fara = resemneaza(exp, iAprobat, (b) => { delete b.approvals; });
cer(!verifyLedger(fara, { policy, policyHash, maxTime: t + 300 }).ok, '5. CONTROL: aprobarile scoase dintr-o intrare permisa, registrul re-semnat de agent -> prins (decizie falsa)');
const forjat = resemneaza(exp, iAprobat, (b) => { const s = Buffer.from(b.approvals[1].signature, 'base64'); s[10] ^= 1; b.approvals[1].signature = s.toString('base64'); });
cer(!verifyLedger(forjat, { policy, policyHash, maxTime: t + 300 }).ok, '5. CONTROL: o semnatura de aprobare falsificata, registrul re-semnat -> prins');
const iRefuzat = exp.entries.findIndex((e) => !e.body.decision.allowed && /0 of 2/.test(e.body.decision.reason));
const mintit = resemneaza(exp, iRefuzat, (b) => { b.decision = { allowed: true, reason: 'approved' }; });
cer(!verifyLedger(mintit, { policy, policyHash, maxTime: t + 300 }).ok, '5. CONTROL: un refuz rescris "permis", re-semnat -> prins');
// adversarul pune aceeasi aprobare de DOUA ori in acelasi registru (a doua intrare, re-semnata): nonce-ul o prinde
const iDubla = exp.entries.findIndex((e) => e.body.decision.allowed && e.body.approvals && e.body.action.amount === '700');
const dubla = resemneaza(exp, iDubla + 1, (b) => { b.action = { ...P700, at: b.at }; b.approvals = pentruL; b.decision = { allowed: true, reason: 'under the limit; approved by 2 of 2' }; delete b.revocation; });
const vDubla = verifyLedger(dubla, { policy, policyHash, maxTime: t + 300 });
cer(!vDubla.ok && vDubla.seq === iDubla + 1 && /0 of 2 valid approvals/.test(vDubla.error), `5. ATAC: aceleasi aprobari folosite a doua oara in acelasi registru, re-semnat -> prins la seq ${vDubla.seq} (nonce folosit: 0 aprobari numarate)`);
// 5b. repornirea agentului: registrul reluat tine minte nonce-urile deja folosite
const LR = resumeLedger({ identity: agent, policy, ledger: L.export(), now });
const rR = LR.record(P500, { approvals: aprobariBune });
cer(!rR.allowed && rR.rejectedApprovals.every((m) => /nonce already used/.test(m)), '5b. dupa repornire, aprobarile deja folosite inainte raman folosite (nonce-urile se refac din registru)');
// 6. revocarea
t = 2000;
cer(!!arunca(() => L.recordRevocation(revoke({ owner: H1, agentId: agent.agentId, policyHash, revokedAt: t }))), '6. CONTROL: o revocare semnata de un aprobator (nu de proprietar) e refuzata de registru');
const R = revoke({ owner: O, agentId: agent.agentId, policyHash, revokedAt: t, reason: 'the owner stops the agent' });
L.recordRevocation(R);
t = 2010;
const r7 = L.record({ kind: 'payment', to: '0xbbbb', amount: '10' });
cer(!r7.allowed && /revoked/.test(r7.reason), `6. dupa revocare, orice actiune e refuzata (${r7.reason})`);
cer(!L.record(P500, { approvals: [ap(H1, P500), ap(H2, P500)] }).allowed, '6. CONTROL: nici doua aprobari valide nu trec peste o revocare');
cer(verifyLedger(L.export(), { policy, policyHash, maxTime: t + 300 }).ok, '6. registrul cu revocarea inregistrata verifica');
const LRv = resumeLedger({ identity: agent, policy, ledger: L.export(), now });
cer(!LRv.record({ kind: 'payment', to: '0xbbbb', amount: '10' }).allowed, '6. dupa repornire, revocarea din registru ramane in vigoare');
// 7. agentul isi omite revocarea: un registru paralel, fara ea, cu actiuni permise dupa revokedAt
t = 1000;
const F = openLedger({ identity: agent, policy, policyHash, now });
F.record({ kind: 'payment', to: '0xbbbb', amount: '50' });
t = 2010;
F.record({ kind: 'payment', to: '0xbbbb', amount: '60' });
const vFara = verifyLedger(F.export(), { policy, policyHash, maxTime: t + 300 });
cer(vFara.ok && vFara.revocations.given === 0, '7. fara revocarile proprietarului, verificatorul NU poate vedea revocarea omisa, si spune ca a judecat 0 revocari');
const vCu = verifyLedger(F.export(), { policy, policyHash, maxTime: t + 300, revocations: [R] });
cer(!vCu.ok && /revoked/.test(vCu.error), `7. cu revocarea proprietarului data SEPARAT, actiunea de dupa ea e prinsa (${vCu.error})`);
const RX = revoke({ owner: X, agentId: agent.agentId, policyHash, revokedAt: 1500 });
const vX = verifyLedger(F.export(), { policy, policyHash, maxTime: t + 300, revocations: [RX] });
cer(vX.ok && vX.revocations.rejected === 1, '7. CONTROL: o "revocare" semnata de un strain nu opreste agentul si e numarata respinsa');
console.log(`\nagent-aprobare: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
process.exitCode = rau ? 1 : 0;