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

101 lines
8.6 KiB
JavaScript

// Proba liniei de comanda a agentilor (agent-cli.mjs), rulata ca un strain: numai fisiere si procese, fara importul modulelor. Fiecare
// drum are perechea lui negativa. Offline; chei ML-DSA-65 reale, generate aici si sterse la sfarsit.
// node proba-agent-cli.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const CLI = path.join(AICI, 'agent-cli.mjs');
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-agent-cli-'));
const f = (n) => path.join(T, n);
let ok = 0, rau = 0;
const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; };
const run = (...a) => { const r = spawnSync(process.execPath, [CLI, ...a], { encoding: 'utf8', timeout: 60000 }); return { cod: r.status, out: (r.stdout || '').trim(), err: (r.stderr || '').trim() }; };
const scrie = (n, o) => { fs.writeFileSync(f(n), JSON.stringify(o)); return f(n); };
// iesirea unei comenzi citita ca JSON; o comanda cazuta (iesire goala) da {} si proba iese ROSIE, nu se opreste (STRICAT)
const J = (s) => { try { return JSON.parse(s); } catch { return {}; } };
try {
// identitati
const ra = run('id', '--out', f('agent'));
const [h1, h2, h3, own] = ['h1', 'h2', 'h3', 'own'].map((n) => run('human', '--out', f(n)));
cer(ra.cod === 0 && /^aere-agent:[0-9a-f]{40}$/.test(ra.out) && [h1, h2, h3, own].every((r) => r.cod === 0 && /^aere-human:[0-9a-f]{40}$/.test(r.out)), '1. id si human scriu cheile si tiparesc numai id-ul');
cer(![ra, h1].some((r) => /PRIVATE KEY/.test(r.out + r.err)), '1. nicio cheie privata pe iesire');
cer(run('id', '--out', f('agent')).cod === 2, '1. CONTROL: o cheie existenta nu se suprascrie (cod 2)');
if (process.platform !== 'win32') cer((fs.statSync(f('agent/agent.key.pem')).mode & 0o777) === 0o600, '1. cheia privata are drepturile 0600');
// politica: 1000 pe ora, aprobare 2 din 3 peste 100, proprietar
const spec = scrie('spec.json', { agentId: ra.out, spend: { amount: '1000', windowSeconds: 3600 }, recipients: ['0xbbbb'],
approval: { approvers: [h1.out, h2.out, h3.out], threshold: 2, above: '100' }, owner: own.out });
const rp = run('policy', '--spec', spec, '--out', f('p.json'));
const P = JSON.parse(fs.readFileSync(f('p.json'), 'utf8'));
cer(rp.cod === 0 && rp.out === P.policyHash && /^0x[0-9a-f]{64}$/.test(P.policyHash), '2. policy scrie politica normala si hash-ul ei');
// check
const a50 = scrie('a50.json', { kind: 'payment', to: '0xbbbb', amount: '50' });
const a500 = scrie('a500.json', { kind: 'payment', to: '0xbbbb', amount: '500' });
cer(run('check', '--policy', f('p.json'), '--action', a50).cod === 0, '3. check: 50 -> permis (0)');
cer(run('check', '--policy', f('p.json'), '--action', scrie('a2000.json', { kind: 'payment', to: '0xbbbb', amount: '2000' })).cod === 3, '3. CONTROL: check 2000 peste limita -> refuzat (3)');
// record, cu reluarea registrului din fisier
const L = f('ledger.json');
const r1 = run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', L, '--action', a50);
cer(r1.cod === 0 && fs.existsSync(L) && J(r1.out).seq === 0, '4. record 50 -> permis, registrul creat (seq 0)');
const r2 = run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', L, '--action', a500);
cer(r2.cod === 3 && /0 of 2/.test(J(r2.out).reason) && J(r2.out).seq === 1, '4. CONTROL: record 500 fara aprobari -> refuzat (3), scris ca refuz la seq 1');
// aprobari
const ap1 = run('approve', '--key', f('h1/human.key.pem'), '--policy', f('p.json'), '--action', a500, '--out', f('ap1.json'));
const ap2 = run('approve', '--key', f('h2/human.key.pem'), '--policy', f('p.json'), '--action', a500, '--out', f('ap2.json'));
cer(ap1.cod === 0 && ap2.cod === 0, '5. doi aprobatori numiti semneaza aprobarea pentru 500');
cer(run('approve', '--key', f('own/human.key.pem'), '--policy', f('p.json'), '--action', a500, '--out', f('ap-own.json')).cod === 1, '5. CONTROL: proprietarul, care nu e aprobator, nu poate aproba (1)');
const r3 = run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', L, '--action', a500, '--approvals', `${f('ap1.json')},${f('ap2.json')}`);
cer(r3.cod === 0 && /approved by 2 of 2/.test(J(r3.out).reason), '5. record 500 cu cele doua aprobari -> permis');
const r4 = run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', L, '--action', a500, '--approvals', `${f('ap1.json')},${f('ap2.json')}`);
cer(r4.cod === 3 && (J(r4.out).rejectedApprovals || [0]).every((m) => /nonce already used/.test(m)), '5. CONTROL: aceleasi aprobari a doua oara (registrul reluat din fisier) -> respinse');
const a600 = scrie('a600.json', { kind: 'payment', to: '0xbbbb', amount: '600' });
const ap3 = run('approve', '--key', f('h1/human.key.pem'), '--policy', f('p.json'), '--action', a600, '--out', f('ap3.json'));
const ap4 = run('approve', '--key', f('h3/human.key.pem'), '--policy', f('p.json'), '--action', a600, '--out', f('ap4.json'));
const r5 = run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', L, '--action', a600, '--approvals', `${f('ap3.json')},${f('ap4.json')}`);
cer(ap3.cod === 0 && ap4.cod === 0 && r5.cod === 3 && /over the limit/.test(J(r5.out).reason), '5. aprobarea nu scuteste de limita, si cheltuiala se tine peste reluari: 50+500+600 > 1000 -> refuzat');
// verificarea ca un strain
const v = run('verify', '--ledger', L, '--policy', f('p.json'));
cer(v.cod === 0 && J(v.out).spent === '550' && J(v.out).humanApproved === 1, '6. verify: registrul verifica, cheltuit 550, o actiune aprobata de oameni');
const atins = JSON.parse(fs.readFileSync(L, 'utf8')); atins.entries[0].body.action.amount = '1';
cer(run('verify', '--ledger', scrie('atins.json', atins), '--policy', f('p.json')).cod === 1, '6. CONTROL: un registru atins -> 1');
const lax = { policy: { ...P.policy, spend: { ...P.policy.spend, amount: '1000000' } }, policyHash: P.policyHash };
const vl = run('verify', '--ledger', L, '--policy', scrie('lax.json', lax));
cer(vl.cod === 1 && /does not hash to the pinned policyHash/.test(vl.out), '6. CONTROL: o politica laxa purtand hash-ul celei reale -> 1');
// revocarea
const t0 = Math.floor(Date.now() / 1000) - 3000;
const rv = run('revoke', '--key', f('own/human.key.pem'), '--policy', f('p.json'), '--at', String(t0), '--reason', 'test', '--out', f('rv.json'));
cer(rv.cod === 0 && fs.existsSync(f('rv.json')), '7. revoke de catre proprietar -> revocare scrisa');
cer(run('revoke', '--key', f('h1/human.key.pem'), '--policy', f('p.json'), '--out', f('rv-rau.json')).cod === 1, '7. CONTROL: revoke cu cheia unui aprobator -> 1');
const vr = run('verify', '--ledger', L, '--policy', f('p.json'), '--revocations', f('rv.json'));
cer(vr.cod === 1 && /revoked/.test(vr.out), '7. verify cu revocarea proprietarului (de dinaintea platilor) -> 1');
// echivocarea: doua continuari diferite ale aceluiasi registru
const A = f('ramura-a.json'), B = f('ramura-b.json');
fs.copyFileSync(L, A); fs.copyFileSync(L, B);
const a10 = scrie('a10.json', { kind: 'payment', to: '0xbbbb', amount: '10' }), a20 = scrie('a20.json', { kind: 'payment', to: '0xbbbb', amount: '20' });
run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', A, '--action', a10);
run('record', '--key', f('agent/agent.key.pem'), '--policy', f('p.json'), '--ledger', B, '--action', a20);
const e = run('equivocation', '--a', A, '--b', B, '--out', f('dovada.json'));
cer(e.cod === 0 && J(e.out).found === true && run('verify-equivocation', '--proof', f('dovada.json')).cod === 0, '8. doua ramuri ale aceluiasi registru -> dovada de echivocare, verificata');
cer(run('equivocation', '--a', A, '--b', L).cod === 1, '8. CONTROL: un registru si prefixul lui nu sunt echivocare (1)');
const d = JSON.parse(fs.readFileSync(f('dovada.json'), 'utf8')); d.b.body.action.amount = '21';
cer(run('verify-equivocation', '--proof', scrie('dovada-rea.json', d)).cod === 1, '8. CONTROL: o dovada cu o intrare nesemnata -> 1');
// folosire gresita
cer(run('nimic').cod === 2 && run('verify', '--ledger', L).cod === 2, '9. CONTROL: comanda necunoscuta sau argument lipsa -> 2');
} catch (e) {
// un fisier care trebuia scris de o comanda si lipseste: drumul s-a rupt, deci proba e ROSIE cu motivul, nu tacuta
cer(false, `proba s-a oprit la un pas al carui fisier lipseste: ${e.message}`);
} finally { fs.rmSync(T, { recursive: true, force: true }); }
console.log(`\nagent-cli: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`);
process.exitCode = rau ? 1 : 0;