#!/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 new agent identity; prints the agentId // node agent-cli.mjs human --out 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 ] [--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 ] // 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; const get = (f) => { const i = rest.indexOf(f); return i >= 0 ? rest[i + 1] : undefined; }; const lista = (f) => (get(f) ? String(get(f)).split(',').filter(Boolean) : []); switch (cmd) { case 'id': case 'human': { const out = get('--out'); if (!out) throw new Folosire(`${cmd} --out `); 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 v = verifyLedger(ledger, { policy: j.policy || j, policyHash: pinned, revocations: lista('--revocations').map((f) => citesteJson(f, 'revocations')), anchors: get('--anchors') ? citesteJson(get('--anchors'), 'anchors') : [], notBefore: get('--not-before') != null ? Number(get('--not-before')) : null }); console.log(JSON.stringify(v, null, 1)); return v.ok ? 0 : 1; } 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) { let cod; try { cod = main(process.argv.slice(2)); } catch (e) { console.error(`agent: ${e.message}`); cod = e instanceof Folosire ? 2 : 1; } process.exitCode = cod; }