aere-quantum/agents/agent-cli.mjs

317 lines
30 KiB
JavaScript

#!/usr/bin/env node
// agent-cli.mjs: linia de comanda a agentilor AI (politica, registrul, aprobarea, revocarea, verificarea, dovada de echivocare), ca
// un om sa poata aproba sau revoca si un strain sa poata verifica fara sa scrie cod. Mesajele si iesirile sunt in engleza; cheile
// private stau in fisierele lor (0600) si nu se tiparesc niciodata. Numai Node 24.
//
// node agent-cli.mjs id --out <dir> new agent identity; prints the agentId
// node agent-cli.mjs human --out <dir> new human identity (approver, owner); prints the humanId
// node agent-cli.mjs policy --spec spec.json [--out p.json] a policy in normal form, with its hash
// node agent-cli.mjs check --policy p.json --action a.json [--spent s.json]
// node agent-cli.mjs record --key agent.key.pem --policy p.json --ledger l.json --action a.json [--approvals a1.json,a2.json] [--provenance f.json]
// node agent-cli.mjs approve --key human.key.pem --policy p.json --action a.json [--valid 600] [--out ap.json]
// node agent-cli.mjs revoke --key owner.key.pem --policy p.json [--at <unix>] [--reason text] [--out rv.json]
// node agent-cli.mjs verify --ledger l.json --policy p.json [--policy-hash 0x..] [--revocations r1.json,r2.json] [--anchors a.json] [--not-before <unix>]
// a.json: anchors {seq, hash, at}, or receipts of the agents witness (martor-agenti.mjs), which then need --rpc <url> and
// --verify-proof <verify-proof.mjs> (or AERE_VERIFY_PROOF) [--chain 28001] [--max-merge-delay <s>]; without --rpc or the verifier such a
// receipt is not measured (exit 2, unless another piece proves a fault of the ledger)
// [--witness aere-witness:<40 hex>[,...]] the witnesses whose receipts are accepted (a receipt of any other, or a plain anchor:
// not measured; no receipt of one: exit 2); without it any ML-DSA-65 key is a witness of
// itself, and the result says the bound is the chain's alone
// [--witness-url <base>] the full list of heads that witness holds for this agent and policy, signed by it page by page within the
// last 300 s, must be entries of this ledger, and every held head needs a verified receipt (taken from the
// address when --anchors has none; a 429 is waited out as its Retry-After asks, within AERE_WITNESS_WAIT_MAX_S
// seconds, 900 by default); no signed list, or one signed by another witness than --witness: not measured
// verify gathers every piece of evidence first, then gives ONE verdict: 1 if a piece proves a fault of the ledger, else 2 if a
// required piece could not be measured, else 0 (README: "verify is a complete evaluation followed by one verdict")
// node agent-cli.mjs equivocation --a l1.json --b l2.json [--out proof.json]
// node agent-cli.mjs verify-equivocation --proof proof.json
// Iesiri: 0 da (permis, valid, gasit), 1 nu (invalid, negasit, refuzat de verificare), 3 actiune refuzata de politica, 2 folosire gresita.
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { definePolicy, hashPolicy, checkAction } from './agent-policy.mjs';
import { newAgentIdentity, agentFromPrivateKeyPem, openLedger, resumeLedger, verifyLedger, findEquivocation, verifyEquivocation } from './agent-ledger.mjs';
import { newHumanIdentity, humanFromPrivateKeyPem, approve, revoke, verifyRevocation } from './agent-aprobare.mjs';
class Folosire extends Error {}
const citesteJson = (f, ce) => {
if (!f) throw new Folosire(`--${ce} is required`);
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${ce} file ${f}: ${e.message}`); }
};
// fisierul unei politici: {policy, policyHash} (cum il scrie `policy`) sau politica singura; hash-ul se recalculeaza mereu
function politica(f) {
const j = citesteJson(f, 'policy');
const { policy, policyHash } = hashPolicy(j.policy || j);
if (j.policyHash && String(j.policyHash).toLowerCase() !== policyHash) throw new Error(`the policy file names policyHash ${j.policyHash}, but the policy hashes to ${policyHash}`);
return { policy, policyHash };
}
function scrieAtomic(f, obiect, mod) {
const tmp = `${f}.${process.pid}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obiect, null, 1) + '\n', mod ? { mode: mod } : undefined);
fs.renameSync(tmp, f);
}
const iese = (obiect, out) => { if (out) scrieAtomic(out, obiect); else console.log(JSON.stringify(obiect, null, 1)); };
const acum = () => Math.floor(Date.now() / 1000);
export function main(argv) {
const [cmd, ...rest] = argv;
// un steag dat fara valoare (pus ultimul, sau urmat de alt steag) ori cu valoarea goala e folosire gresita (2), nu un steag nedat (revizia B3 a
// treia, R3-03): pana atunci `--witness-url` sau `--policy-hash` puse ultimele, `--revocations ""` (o variabila goala) scoteau in tacere lista
// martorului, pinul politicii sau revocarile, si un registru prins iesea 0. Numai `--reason` are voie sa fie gol.
const GOALE_PERMISE = ['--reason'];
const get = (f) => { const i = rest.indexOf(f); if (i < 0) return undefined; const v = rest[i + 1]; if (!(v !== undefined && !/^--[a-z]/.test(v) && (v !== '' || GOALE_PERMISE.includes(f))) /*PAZA-MARTOR:steag-fara-valoare*/) throw new Folosire(`${f} needs a value`); return v; };
// (un element gol din lista, `a.json,`, ajunge la citesteJson, care il refuza ca folosire gresita)
const lista = (f) => { const v = get(f); return v === undefined ? [] : String(v).split(','); };
// toate aparitiile unui steag repetabil (--witness a --witness b, sau --witness a,b)
const toate = (f) => rest.flatMap((x, i) => (x === f ? (rest[i + 1] === undefined ? [''] : String(rest[i + 1]).split(',')) : []));
// un numar intreg pozitiv; altfel folosire gresita (revizia B3, constatarea 5: `--chain 2800x` dadea NaN si paza lantului cadea DESCHISA)
const intregPozitiv = (f) => { const v = get(f); if (v === undefined) return undefined; if (!/^[1-9]\d{0,14}$/.test(String(v))) throw new Folosire(`${f} must be a positive integer, not "${v}"`); return Number(v); };
// STEAGURILE CUNOSCUTE ale fiecarei comenzi (revizia B3 a patra, R4-05): argumentele vin in perechi steag + valoare, si un argument care nu e un
// steag al comenzii (`--witness-url=<URL>`, o greseala de tastare ca `--ledgr`, o valoare ramasa fara steag) e folosire gresita (2). Pana atunci
// cititorul cauta potrivirea exacta si nu refuza nimic necunoscut, deci `--witness-url=<URL>` sau `--policy-hash=<h>` erau "nedate" si paza lor
// cadea in tacere, cu exit 0. Un steag dat de doua ori e si el folosire gresita, afara de --witness (repetabil). Valorile le judeca `get`.
const STEAGURI = { id: ['--out'], human: ['--out'], policy: ['--spec', '--out'], check: ['--policy', '--action', '--spent'],
record: ['--key', '--policy', '--ledger', '--action', '--approvals', '--provenance'], approve: ['--key', '--policy', '--action', '--valid', '--out'],
revoke: ['--key', '--policy', '--at', '--reason', '--out'],
verify: ['--ledger', '--policy', '--policy-hash', '--revocations', '--anchors', '--not-before', '--rpc', '--verify-proof', '--chain', '--max-merge-delay', '--witness', '--witness-url'],
equivocation: ['--a', '--b', '--out'], 'verify-equivocation': ['--proof'] };
if (Object.hasOwn(STEAGURI, cmd)) {
const vazute = new Set();
for (let i = 0; i < rest.length; i += 2) {
const s = String(rest[i]);
if (!(STEAGURI[cmd].includes(s) /*PAZA-MARTOR:steag-necunoscut*/)) throw new Folosire(`${cmd}: unknown argument ${JSON.stringify(s.slice(0, 60))} (its flags: ${STEAGURI[cmd].join(' ')}; the value follows its flag, never --flag=value)`);
if (!(!vazute.has(s) || s === '--witness') /*PAZA-MARTOR:steag-dublu*/) throw new Folosire(`${cmd}: ${s} is given twice`);
vazute.add(s);
}
}
switch (cmd) {
case 'id':
case 'human': {
const out = get('--out'); if (!out) throw new Folosire(`${cmd} --out <dir>`);
const x = cmd === 'id' ? newAgentIdentity() : newHumanIdentity();
const nume = cmd === 'id' ? 'agent' : 'human';
fs.mkdirSync(out, { recursive: true });
const cheie = path.join(out, `${nume}.key.pem`);
if (fs.existsSync(cheie)) throw new Folosire(`${cheie} exists; refusing to overwrite a key`);
fs.writeFileSync(cheie, x.privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
fs.writeFileSync(path.join(out, `${nume}.pub.pem`), x.publicKeyPem, { mode: 0o644 });
console.log(cmd === 'id' ? x.agentId : x.humanId);
return 0;
}
case 'policy': {
const r = definePolicy(citesteJson(get('--spec'), 'spec'));
iese(r, get('--out')); if (get('--out')) console.log(r.policyHash);
return 0;
}
case 'check': {
const { policy } = politica(get('--policy'));
const action = citesteJson(get('--action'), 'action');
const spent = get('--spent') ? citesteJson(get('--spent'), 'spent') : [];
const d = checkAction(policy, { ...action, at: action.at ?? acum() }, spent);
console.log(JSON.stringify(d)); return d.allowed ? 0 : 3;
}
case 'record': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const identity = agentFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
const lf = get('--ledger'); if (!lf) throw new Folosire('--ledger is required');
const L = fs.existsSync(lf) ? resumeLedger({ identity, policy, policyHash, ledger: citesteJson(lf, 'ledger') }) : openLedger({ identity, policy, policyHash });
const action = citesteJson(get('--action'), 'action'); // momentul il pune registrul (record suprascrie `at`)
const approvals = lista('--approvals').map((f) => citesteJson(f, 'approvals'));
const provenance = get('--provenance') ? citesteJson(get('--provenance'), 'provenance') : undefined;
const r = L.record(action, { approvals, provenance });
scrieAtomic(lf, L.export());
console.log(JSON.stringify({ allowed: r.allowed, reason: r.reason, seq: r.entry.seq, hash: r.entry.hash, rejectedApprovals: r.rejectedApprovals }));
return r.allowed ? 0 : 3;
}
case 'approve': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const human = humanFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
if (!policy.approval || !policy.approval.approvers.includes(human.humanId)) throw new Error(`${human.humanId} is not an approver named in the policy`);
const action = citesteJson(get('--action'), 'action');
const valid = Number(get('--valid') ?? 600);
if (!Number.isInteger(valid) || valid < 1 || valid > 7 * 86400) throw new Folosire('--valid must be 1..604800 seconds');
const t = acum();
iese(approve({ human, agentId: policy.agentId, policyHash, action, issuedAt: t, expiresAt: t + valid }), get('--out'));
return 0;
}
case 'revoke': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const owner = humanFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
const rv = revoke({ owner, agentId: policy.agentId, policyHash, revokedAt: Number(get('--at') ?? acum()), reason: get('--reason') ?? '' });
const r = verifyRevocation(rv, { policy, policyHash });
if (!r.ok) { console.error(`agent: the revocation would not be valid: ${r.error}`); return 1; }
iese(rv, get('--out')); return 0;
}
case 'verify': {
const ledger = citesteJson(get('--ledger'), 'ledger');
const j = citesteJson(get('--policy'), 'policy');
const pinned = get('--policy-hash') ?? j.policyHash;
const revocari = lista('--revocations').map((f) => citesteJson(f, 'revocations'));
const notBefore = get('--not-before') != null ? Number(get('--not-before')) : null;
const date = get('--anchors') ? citesteJson(get('--anchors'), 'anchors') : [];
const toate2 = Array.isArray(date) ? date : [date];
// argumentele martorului se judeca INAINTE de orice: o valoare gresita e folosire gresita (2), niciodata o paza care cade deschisa
const chain = intregPozitiv('--chain'), mergeMax = intregPozitiv('--max-merge-delay');
const martori = toate('--witness');
if (martori.some((w) => !/^aere-witness:[0-9a-f]{40}$/.test(w))) throw new Folosire('--witness must be aere-witness:<40 hex> (repeatable, or comma-separated)');
const urlMartor = get('--witness-url');
if (urlMartor !== undefined && !/^https?:\/\/[^\s]+$/.test(String(urlMartor))) throw new Folosire('--witness-url must be the base URL of a witness (http:// or https://)');
const rpc = get('--rpc'), verifyProof = get('--verify-proof') || process.env.AERE_VERIFY_PROOF;
// bugetul de asteptare pe 429 (R5-06): un numar intreg de secunde; o valoare gresita e folosire gresita, nu o tacere pe valoarea implicita
const asteptareMax = process.env.AERE_WITNESS_WAIT_MAX_S;
if (asteptareMax !== undefined && asteptareMax !== '' && !/^\d{1,6}$/.test(asteptareMax)) throw new Folosire('AERE_WITNESS_WAIT_MAX_S must be a whole number of seconds (0 to 999999)');
// o chitanta fara --rpc (din --anchors sau adusa) e o dovada NEMASURATA cu regula `rpc`, judecata la locul ei, dupa tot ce nu cere lantul (lista,
// capetele tinute, ancorele); pana la a sasea revizie (N1) o chitanta din --anchors fara --rpc iesea aici cu 2 (folosire gresita), INAINTEA listei
// semnate, deci o ramura aratata de lista se ascundea punand in --anchors orice obiect cu forma de chitanta
const ob = (x) => !!x && typeof x === 'object' && !Array.isArray(x); // ca esteChitanta din martor-agenti.mjs (incarcat abia mai jos)
const esteChitanta = (a) => ob(a) && (ob(a.witnessReceipt) || (ob(a.receipt) && ob(a.receipt.witnessReceipt)));
// o chitanta a martorului gazduit (martor-agenti.mjs) e primita ca ancora numai dupa ce ESTE verificata aici: includerea refacuta local si
// momentul capului de jurnal certificat de verificatorul AIP-23 pe lant
const dinMartor = (a) => !!a && typeof a === 'object' && !Array.isArray(a) && ['witnessReceipt', 'receipt', 'promise', 'status'].some((k) => a[k] !== undefined);
const ancoraSimpla = (a) => !!a && typeof a === 'object' && !Array.isArray(a) && !dinMartor(a) && Number.isSafeInteger(a.seq) && a.seq >= 0 && typeof a.hash === 'string' && /^[0-9a-f]{64}$/.test(a.hash) && Number.isSafeInteger(a.at);
const txt = (x) => (typeof x === 'string' ? x.slice(0, 120) : `<${x === null ? 'null' : Array.isArray(x) ? 'array' : typeof x}>`);
return (async () => {
// ================================================================================================================================
// EVALUAREA COMPLETA, UN SINGUR VERDICT (revizia B3 a cincea, 2026-10-19, R5-01..R5-05; scrisa si in README). Pana atunci verificarea
// decidea PE DRUM, iesind cu 2 sau 1 la primul semn, in ordinea citirii, iar ordinea o alegea agentul: un cap proaspat in asteptare, un
// {"status":"pending"} in --anchors sau o copie veche a unei chitante ascundeau o antedatare DOVEDITA (R5-01), iar o chitanta cazuta din
// vina notarului dadea 1 predata si 2 adusa (R5-03). Acum:
// 1. Se ADUNA toate dovezile: fiecare intrare din --anchors (orice forma), lista semnata a martorului, fiecare cap tinut, chitanta
// adusa de la adresa pentru fiecare cap tinut fara o chitanta VERIFICATA in --anchors. Nimic nu iese din functie in timpul adunarii.
// 2. Fiecare dovada primeste o CLASA: VERIFICATA (tine, da o margine), VINA-REGISTRULUI (dovedita: un cap tinut de martor care nu e
// intrare a registrului, o echivocare pastrata, o chitanta a martorului fixat legata de alt registru, o chitanta predata de agent cu
// semnatura martorului pe care il numeste INVALIDA, o intrare antedatata fata de o margine din dovezi VERIFICATE), NEMASURATA
// (orice altceva: in asteptare, forma gresita, notarul in urma, finalitatea data de RPC-ul verificatorului, o lista semnata de alt
// martor decat cel fixat, o ancora simpla sub un martor fixat, o adresa care strica raspunsul).
// 3. VERDICTUL: FAILED daca exista o VINA-REGISTRULUI; altfel NOT MEASURED daca o dovada CERUTA e NEMASURATA; altfel OK. Deci o vina
// dovedita nu poate fi ascunsa de un NEMASURAT, oricat de devreme vine, iar o pana a martorului nu face dintr-un registru cinstit un
// FAILED. O dovada e CERUTA afara de o chitanta (sau un raspuns in asteptare) din --anchors al carei cap are deja o chitanta VERIFICATA.
// REGULA (c): nicio forma de raspuns nu arunca; tipurile se judeca inainte de folosire (martor-agenti.mjs: sir(), chitanta-tipuri).
// ================================================================================================================================
// modulul martorului se incarca numai cand o dovada sau un steag il cere: un registru cu ancore simple (sau fara) se verifica si fara el
const M = urlMartor !== undefined || martori.length > 0 || toate2.some(dinMartor) ? await import('./martor-agenti.mjs') : null;
const dovezi = [];
const adauga = (d) => { dovezi.push({ ceruta: true, ...d }); };
let fixati = martori.length ? martori : null;
const fixare = !!fixati || urlMartor !== undefined;
// buget comun de asteptare pe 429 (Retry-After), pentru lista si chitantele aduse (R5-06)
const buget = { ramasS: asteptareMax ? Number(asteptareMax) : 900, asteptatS: 0 };
const cere = M ? M.cereRabdator(buget) : null;
// 1a. lista semnata a capetelor (sub --witness-url)
let tinute = null;
if (urlMartor !== undefined) {
const c = await M.citesteCapeteMartorului(urlMartor, ledger.agentId, ledger.session, { cere });
if (!c.ok) adauga({ sursa: 'list', clasa: 'NEMASURATA', detail: `the witness at --witness-url did not give a list of heads it signed: ${c.motiv}` });
else if (!(!fixati || fixati.includes(c.witness)) /*PAZA-MARTOR:martor-url*/) adauga({ sursa: 'list', clasa: 'NEMASURATA', rule: 'martor', detail: `the list at --witness-url is signed by ${c.witness}, not by a pinned witness (${fixati.join(', ')}): not the testimony of the pinned witness (another address, or a server without its key)` });
else {
fixati = fixati || [c.witness];
const vc = M.verificaCapeteMartorului(ledger, c);
for (const x of vc.randuri) adauga({ sursa: `list of ${c.witness} (signed at ${c.at})`, clasa: x.pass === true ? 'VERIFICATA' : x.pass === false ? 'VINA' : 'NEMASURATA', rule: x.rule, seq: x.seq, detail: x.detail });
tinute = c.heads;
}
}
// o chitanta, din --anchors (`agent`) sau adusa de la adresa (`adusa`), judecata local si apoi pe lant; fara exceptie pe nicio forma
const idCap = (a) => { const p = M.chitantaDin(a); const w = p && typeof p === 'object' ? p.witnessReceipt : null; const h = w && typeof w === 'object' ? w.head : null; return h && typeof h === 'object' && typeof h.statementHash === 'string' ? h.statementHash : null; } /*PAZA-MARTOR:chitanta-id-sigur*/;
const areVerificator = !!verifyProof && fs.existsSync(verifyProof);
const o = { verifyProof, rpc, chain: chain ?? null, mergeMax, martori: null, verdicte: new Map() };
const evalueaza = async (x, sursa) => {
const plic = M.chitantaDin(x);
const l = M.verificaChitanta(plic, { ledger, martori: fixati });
if (!l.ok) {
// vina registrului: o chitanta a martorului fixat legata de alt registru (alt agent, alta politica, alta ramura); si, numai pentru una PREDATA
// de agent, o semnatura INVALIDA a martorului pe care il numeste (cand e fixat, sau cand nu e fixat niciunul)
const vp = M.verificaPunct(plic);
const numit = vp.rule === 'martor' || vp.rule === 'punct-semnatura' ? plic.statement.witness : null;
const falsificata = sursa === 'agent' && numit !== null && (!fixati || fixati.includes(numit)) /*PAZA-MARTOR:chitanta-falsificata*/;
const vina = l.rule === 'registru' /*PAZA-MARTOR:chitanta-alt-registru*/ || falsificata;
return { chitanta: true, clasa: vina ? 'VINA' : 'NEMASURATA', rule: l.rule, detail: l.error };
}
if (!rpc /*PAZA-MARTOR:fara-rpc*/) return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA', rule: 'rpc', detail: 'a witness receipt needs --rpc <url>: the chain is read, not trusted from the receipt' };
if (!areVerificator) return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA', rule: 'lant', detail: 'a witness receipt needs the AIP-23 reference verifier (--verify-proof <verify-proof.mjs> or AERE_VERIFY_PROOF)' };
// pe lant: nenotarizata, intarziata, cu finalitatea refuzata de RPC-ul ales de verificator: vina martorului, a notarului sau a RPC-ului, nu a
// registrului, pe ambele drumuri (R5-03: pana atunci o chitanta predata de agent dadea aici FAILED, iar aceeasi, adusa, NEMASURAT)
const r = await M.ancoreDinChitante(ledger, [plic], { ...o, martori: fixati });
const rd = r.randuri[0] || {};
if (r.stare === 'OK' && r.anchors.length === 1) return { chitanta: true, seq: l.head.seq, clasa: 'VERIFICATA', ancora: r.anchors[0], detail: rd.detail };
return { chitanta: true, seq: l.head.seq, clasa: 'NEMASURATA' /*PAZA-MARTOR:lant-nemasurat*/, rule: rd.rule || 'lant', detail: rd.detail || 'not judged' };
};
// 1b. fiecare intrare din --anchors, oricare ar fi forma ei
const fazaAnchors = async () => { for (const [i, a] of toate2.entries()) {
const sursa = `anchors[${i}]`;
if (esteChitanta(a)) { const e = await evalueaza(a, 'agent'); adauga({ sursa, id: idCap(a), ...e }); continue; }
if (dinMartor(a)) { adauga({ sursa, id: typeof a.id === 'string' && /^0x[0-9a-f]{64}$/.test(a.id) ? a.id : null, clasa: 'NEMASURATA', rule: 'fara-chitanta', detail: `an answer of the witness that carries no receipt yet (status ${txt(a.status)}): ask the witness again after its next checkpoint` }); continue; }
if (ancoraSimpla(a)) {
if (fixare /*PAZA-MARTOR:martor-simple*/) adauga({ sursa, seq: a.seq, clasa: 'NEMASURATA', rule: 'martor', detail: 'a plain anchor {seq, hash, at} under a pinned witness is not the testimony of the witness (anchors of your own run, such as agent-ancora.mjs anchors, are judged in a run without --witness and --witness-url)' });
else adauga({ sursa, seq: a.seq, clasa: 'VERIFICATA', ancora: { seq: a.seq, hash: a.hash, at: a.at }, detail: `a plain anchor at seq ${a.seq}, taken as the verifier's own (no witness pinned)` });
continue;
}
adauga({ sursa, clasa: 'NEMASURATA', rule: 'forma', detail: `not a piece of evidence: neither a receipt, nor an answer of the witness, nor an anchor {seq, hash, at} (${txt(a)})` });
} };
// 1c. fiecare cap tinut de martor, cu o chitanta VERIFICATA: a agentului, altfel adusa de la adresa (oricare cap, nu doar ultimul; o chitanta a
// agentului cu acelasi id care nu tine nu tine locul celei verificate, R5-01)
const descrie = (r) => (!r.status ? `the witness did not answer (${txt(r.eroare)})` : r.status === 429 ? `the witness answered HTTP 429 (Retry-After ${r.retryAfter === null ? 'none' : r.retryAfter + ' s'}; ${buget.asteptatS} s already waited of a budget of ${buget.asteptatS + buget.ramasS} s, AERE_WITNESS_WAIT_MAX_S)`
: r.status !== 200 ? `the witness answered HTTP ${r.status}` : !(r.j && typeof r.j === 'object' && !Array.isArray(r.j)) ? 'the answer is not the state of a head (not JSON)'
: !M.esteChitanta(r.j) ? (typeof r.j.status === 'string' && r.j.status !== 'notarized' ? `the witness has it ${txt(r.j.status)}, not notarized yet (ask again after its checkpoint is notarized)` : 'the answer carries no receipt')
: idCap(r.j) === null ? 'the receipt in the answer has no head' : 'the answer is the receipt of another head');
const fazaCapete = async () => { for (const h of tinute || [] /*PAZA-MARTOR:martor-fiecare-cap*/) {
if (dovezi.some((d) => d.clasa === 'VERIFICATA' && d.id === h.id) /*PAZA-MARTOR:martor-chitanta-ceruta*/) continue;
const sursa = `seq ${h.seq}, taken from --witness-url`;
const r = await M.stareaCapuluiLa(urlMartor, h.id, { cere });
if (!(r.status === 200 && M.esteChitanta(r.j) && idCap(r.j) === h.id)) { adauga({ sursa, seq: h.seq, id: h.id, clasa: 'NEMASURATA', rule: 'adusa', detail: `the witness holds seq ${h.seq} and no verified receipt of it was in --anchors; ${descrie(r)}: the entries after seq ${h.seq} are not bounded by the witness` }); continue; }
const e = await evalueaza(r.j, 'adusa');
adauga({ sursa, seq: h.seq, id: h.id, ...e, detail: e.clasa === 'NEMASURATA' ? `${e.detail}: the entries after seq ${h.seq} are not bounded by the witness` : e.detail });
} };
// adunarea: --anchors, apoi capetele tinute (o chitanta VERIFICATA a agentului scuteste aducerea); ordinea nu schimba verdictul, numai cate se aduc
await fazaAnchors(); await fazaCapete();
// 1d. --witness cere o legatura SEMNATA de martorul fixat: fara nicio chitanta de judecat, marginea martorului nu e masurata
if (!(!martori.length || dovezi.some((d) => d.chitanta) || (tinute && tinute.length)) /*PAZA-MARTOR:martor-fara-chitanta*/) adauga({ sursa: 'witness', clasa: 'NEMASURATA', rule: 'martor', detail: '--witness was given, but there is no receipt of a pinned witness: the bound of the witness was not measured' });
// o chitanta (sau un raspuns in asteptare) din --anchors al carei cap are o chitanta VERIFICATA nu mai e ceruta (o copie veche)
for (const d of dovezi) if (d.clasa === 'NEMASURATA' && d.id && dovezi.some((x) => x !== d && x.clasa === 'VERIFICATA' && x.id === d.id) /*PAZA-MARTOR:copie-veche*/) d.ceruta = false;
// 2. registrul, judecat cu ancorele VERIFICATE (o antedatare fata de ele e vina registrului)
const ancore = dovezi.filter((d) => d.clasa === 'VERIFICATA' && d.ancora).map((d) => d.ancora).sort((a, b) => a.seq - b.seq);
const v = verifyLedger(ledger, { policy: j.policy || j, policyHash: pinned, revocations: revocari, anchors: ancore, notBefore });
if (!v.ok) adauga({ sursa: 'ledger', clasa: 'VINA', rule: v.rule, detail: v.error });
// 3. UN SINGUR verdict, dupa ce s-a adunat tot
const vina = dovezi.filter((d) => d.clasa === 'VINA'), nemasurate = dovezi.filter((d) => d.clasa === 'NEMASURATA' && d.ceruta);
const verdict = vina.length ? 'FAILED' : nemasurate.length ? 'NOT MEASURED' : 'OK' /*PAZA-MARTOR:verdict-vina-intai*/;
for (const d of dovezi) console.error(`${d.clasa === 'VERIFICATA' ? 'ok' : d.clasa === 'VINA' ? 'FAIL' : d.ceruta ? '--' : '..'} ${d.sursa}: ${d.rule ? '[' + d.rule + '] ' : ''}${d.detail}${d.ceruta ? '' : ' (superseded by a verified receipt of the same head)'}`);
const chit = ancore.filter((a) => a.witness);
if (chit.length) {
v.witnessReceipts = chit.map((a) => ({ seq: a.seq, witness: a.witness, pinned: a.witnessPinned, chainLowerBound: a.chainLowerBound }));
if (v.time && chit.some((a) => !a.witnessPinned)) v.time.lowerBound = 'witnessed by an UNPINNED witness: the chain alone bounds each head to no earlier than its chainLowerBound (F minus the merge delay minus 600 s); pass --witness to pin the witness';
}
if (!fixati && chit.length) console.error('NOTE: no witness was pinned (--witness or --witness-url): each receipt above bounds its head by the chain alone, not by the witness clock');
if (tinute && !tinute.length && verdict === 'OK') console.error('NOTE: the witness holds no head of this ledger: the list was checked, but the time of the entries is not bounded by the witness');
if (buget.asteptatS) console.error(`NOTE: the witness rate-limited this verification (HTTP 429): ${buget.asteptatS} s waited, as its Retry-After asked, of a budget of ${buget.asteptatS + buget.ramasS} s (AERE_WITNESS_WAIT_MAX_S)`);
v.verdict = verdict; v.ok = verdict === 'OK' ? true : verdict === 'FAILED' ? false : null;
v.evidence = dovezi.map((d) => ({ source: d.sursa, class: d.clasa === 'VINA' ? 'LEDGER-FAULT' : d.clasa === 'VERIFICATA' ? 'VERIFIED' : 'NOT-MEASURED', required: d.ceruta, ...(d.seq !== undefined ? { seq: d.seq } : {}), ...(d.rule ? { rule: d.rule } : {}) }));
if (verdict === 'FAILED') console.error(`FAILED: ${vina.length} piece(s) of evidence show a fault of the ledger (${[...new Set(vina.map((d) => d.rule || 'ledger'))].join(', ')})`);
if (verdict === 'NOT MEASURED') console.error(`NOT MEASURED: ${nemasurate.length} required piece(s) of evidence could not be verified, and none shows a fault of the ledger (${[...new Set(nemasurate.map((d) => d.rule || 'list'))].join(', ')})`);
console.log(JSON.stringify(v, null, 1));
return verdict === 'OK' ? 0 : verdict === 'FAILED' ? 1 : 2;
})();
}
case 'equivocation': {
const p = findEquivocation(citesteJson(get('--a'), 'a'), citesteJson(get('--b'), 'b'));
if (!p) { console.log(JSON.stringify({ found: false })); return 1; }
iese(p, get('--out')); if (get('--out')) console.log(JSON.stringify({ found: true, seq: p.seq })); return 0;
}
case 'verify-equivocation': {
const r = verifyEquivocation(citesteJson(get('--proof'), 'proof'));
console.log(JSON.stringify(r)); return r.ok ? 0 : 1;
}
default:
console.error('usage: agent-cli.mjs id|human|policy|check|record|approve|revoke|verify|equivocation|verify-equivocation ... (see README.md)');
return cmd ? 2 : 0;
}
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
// main intoarce un cod, sau (verify cu o chitanta a martorului, care citeste lantul) o promisiune de cod
Promise.resolve().then(() => main(process.argv.slice(2)))
.then((cod) => { process.exitCode = cod; }, (e) => { console.error(`agent: ${e.message}`); process.exitCode = e instanceof Folosire ? 2 : 1; });
}