// 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;