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.
101 lines
8.6 KiB
JavaScript
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;
|