diff --git a/README.md b/README.md index e56875d..4bed869 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Aere Quantum Self-hosted post-quantum infrastructure from Aere Network. Each component is a few files with **no dependencies**: Node.js 24 -and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. The one exception is the notarization -command of the verification layer, which needs `ethers`. +and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. The two exceptions need `ethers`: the +notarization command of the verification layer, and the agents' x402 wallet (EIP-712 and secp256k1). | component | what it does | |---|---| @@ -14,7 +14,7 @@ command of the verification layer, which needs `ethers`. | [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again | -| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check | +| [`agents/`](agents/) | limits an AI agent cannot break unseen: a post-quantum identity (ML-DSA-65), a policy (spending per time window, allowed tools and recipients, which actions need human approval), a signed ledger of every action judged against the policy, approvals and revocation signed by people, and a verifier that re-runs the policy over the whole ledger without trusting the agent; two branches of one ledger are a proof of equivocation anyone can check; and a wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet with its evidence | Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them. @@ -34,7 +34,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | | control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8 | remediation 7/7, compliance report 3/3 in this repository | -| agents | policy 23/23 with the AIP-23 verifier (without it 21 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here) | 25/25 (`node control-negativ-aprobare.mjs`) | +| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25 and payment verifier 10/10 without a network; on the public testnet 28001, 9/9 (`x402/proba-x402-testnet.mjs`, needs a funded testnet key) | 26/26 (`node control-negativ-aprobare.mjs`); x402 21/21 (`node x402/control-negativ-wallet.mjs`) | Code comments, most function and variable names (also many exported between the files of a component), test names and control messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error @@ -43,4 +43,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 108 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 17, agents 10, readiness 4). +MIT, see [LICENSE](LICENSE). Files: 122 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 17, agents 24, readiness 4). diff --git a/agents/README.md b/agents/README.md index fe85522..b224962 100644 --- a/agents/README.md +++ b/agents/README.md @@ -25,7 +25,8 @@ node agent-cli.mjs verify-equivocation --proof proof.json ``` A spec is `{ agentId, spend: { amount, windowSeconds, asset? }, tools: [...], recipients: [...], approval: { approvers: [humanId...], -threshold, above?, tools? }, owner: humanId }`; every field but `agentId` is optional. An action is `{ kind: "payment", to, amount, +threshold, above?, tools? }, owner: humanId, wallet: 0x }`; every field but `agentId` is optional. When `wallet` is +named, a payment must say `from` and be from it. An action is `{ kind: "payment", to, amount, asset? }` or `{ kind: "tool", tool, args? }`; the ledger sets its time. Exit codes: 0 yes (allowed, valid, found), 1 no (invalid, not found, refused by a check), 3 an action refused by the policy, 2 wrong usage. Private keys stay in their files and are never printed; the tool creates them with mode 0600, which has no effect on Windows (protect the folder there). @@ -51,6 +52,12 @@ printed; the tool creates them with mode 0600, which has no effect on Windows (p a validity window of at most seven days and a nonce that counts once per ledger. Only approvers named in the policy count, each once. A revocation is signed by the owner named in the policy; from its time on every action is refused. +## Paying over x402 + +`x402/` holds the agent wallet: a service that keeps the payment key for the owner and signs an x402 payment only when the agent's +ledger records it and the policy allows it, with the wallet as the witness of the ledger's heads and time. It was run end to end +on the public testnet 28001 on 2026-09-29, and the evidence is there with a verifier anyone can run. See `x402/README.md`. + ## What the verifier can and cannot see - **Time.** An entry's time is the statement of whoever holds the agent key. The verifier bounds it from above with its clock. It @@ -72,11 +79,11 @@ Measured on 2026-09-29 (Node.js 24.14.1, Windows): | test | result | |---|---| -| `node proba-agent-policy.mjs` | 23/23 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=`); without it 21 run, 2 are reported as skipped and the exit code is 2 | +| `node proba-agent-policy.mjs` | 27/27 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=`); without it 25 run, 2 are reported as skipped and the exit code is 2 | | `node proba-agent-ledger.mjs` | 51/51: limits over the ledger, identity, tampering, a re-signed false decision, a looser policy under the real hash, backdating (refused when written; caught with anchors or `notBefore`), a branch, equivocation proofs, resuming | | `node proba-agent-aprobare.mjs` | 39/39: threshold, unnamed or repeated approvers, classical keys, another action or other tool arguments, reused nonces (also after a restart), expiry, approvals across branches, revocation, an omitted revocation | | `node proba-agent-cli.mjs` | 23/23 on Windows, through files and processes only; on Linux and macOS one more test checks the 0600 mode of the key (not measured here) | -| `node control-negativ-aprobare.mjs` | 25/25: each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control | +| `node control-negativ-aprobare.mjs` | 26/26: each guard is removed in a copy, one at a time, and the named test must fail for that reason; a test that stops before its summary counts as a failure of the control | The data format is version 2 (2026-09-29). Code comments, test names and most internal names are in Romanian; messages, data and the exported interface are in English. No third party has reviewed this component. diff --git a/agents/agent-policy.mjs b/agents/agent-policy.mjs index c4e4015..0c1a7af 100644 --- a/agents/agent-policy.mjs +++ b/agents/agent-policy.mjs @@ -20,7 +20,7 @@ export function canonical(v) { return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}'; } -const CHEI_POLITICA = new Set(['v', 'kind', 'agentId', 'spend', 'tools', 'recipients', 'approval', 'owner']); +const CHEI_POLITICA = new Set(['v', 'kind', 'agentId', 'spend', 'tools', 'recipients', 'approval', 'owner', 'wallet']); const HUMAN_ID = /^aere-human:[0-9a-f]{40}$/; const INTREG = /^[0-9]+$/; @@ -57,6 +57,12 @@ function formaNormala(p) { if (!HUMAN_ID.test(String(p.owner))) throw new Error('agent-policy: owner must be an aere-human: id'); policy.owner = String(p.owner); } + // 2026-09-29 (punctul 23/25): portofelul din care agentul are voie sa plateasca (o adresa EVM). Cand e numit, o plata trebuie sa + // spuna `from` si sa fie chiar el; x402/wallet.mjs refuza sa semneze pentru o politica ce numeste alt portofel. + if (p.wallet != null) { + if (!/^0x[0-9a-fA-F]{40}$/.test(String(p.wallet))) throw new Error('agent-policy: wallet must be an EVM address (0x + 40 hex)'); + policy.wallet = String(p.wallet).toLowerCase(); + } return policy; } const hashFormei = (policy) => sha256(Buffer.from(JSON.stringify(policy), 'utf8')); @@ -72,6 +78,7 @@ const hashFormei = (policy) => sha256(Buffer.from(JSON.stringify(policy), 'utf8' * o plata cu suma STRICT peste `above`, sau o unealta din `tools`, cere `threshold` aprobari de la aprobatori distincti * din `approvers` (id-uri 'aere-human:' derivate din cheile lor ML-DSA-65; vezi agent-aprobare.mjs) * @param {string} [p.owner] proprietarul ('aere-human:'), singurul care poate revoca agentul + * @param {string} [p.wallet] (2026-09-29) adresa EVM din care agentul are voie sa plateasca; o plata trebuie sa o numeasca in `from` */ export function definePolicy(p) { const policy = formaNormala(p); @@ -133,6 +140,7 @@ function faraAprobare(policy, action, spentInWindow) { // si un moment finit; altfel refuz, cu motivul numit. if (!INTREG.test(String(action.amount)) || BigInt(action.amount) === 0n) return { allowed: false, reason: `invalid amount (${String(action.amount).slice(0, 24)}): a strictly positive decimal integer is required` }; if (!Number.isFinite(Number(action.at))) return { allowed: false, reason: 'invalid time: action.at is not a finite number' }; + if (policy.wallet && String(action.from || '').toLowerCase() !== policy.wallet) return { allowed: false, reason: `payment from ${String(action.from || 'an unnamed wallet').slice(0, 42)} is not from the wallet the policy names` }; if (policy.recipients && !policy.recipients.includes(String(action.to).toLowerCase())) return { allowed: false, reason: `recipient ${action.to} is not allowed` }; if (policy.spend) { // 2026-09-29: o plata in alt activ decat al limitei nu se aduna la ea si nu trece pe langa ea: e refuzata diff --git a/agents/control-negativ-aprobare.mjs b/agents/control-negativ-aprobare.mjs index 4e7cd39..40459a1 100644 --- a/agents/control-negativ-aprobare.mjs +++ b/agents/control-negativ-aprobare.mjs @@ -10,7 +10,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { spawnSync } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); @@ -41,33 +41,48 @@ const PLANTARI = [ ['linia de comanda lasa pe oricine sa aprobe', 'agent-cli.mjs', 'if (!policy.approval || !policy.approval.approvers.includes(human.humanId)) throw', 'if (false) throw', P.cli], ['verify din linia de comanda uita hash-ul numit in fisierul politicii', 'agent-cli.mjs', "const pinned = get('--policy-hash') ?? j.policyHash;", "const pinned = get('--policy-hash');", P.cli], ['record deschide mereu un registru nou in loc sa il reia', 'agent-cli.mjs', 'const L = fs.existsSync(lf) ? resumeLedger(', 'const L = false ? resumeLedger(', P.cli], + ['o plata din alt portofel decat cel numit trece', 'agent-policy.mjs', 'if (policy.wallet && String(action.from', 'if (false && String(action.from', P.pol], ['politica cu un camp necunoscut e primita', 'agent-policy.mjs', 'if (necunoscute.length) throw', 'if (false) throw', P.pol], ['o plata in alt activ trece pe langa limita', 'agent-policy.mjs', 'if (action.asset != null && String(action.asset) !== policy.spend.asset) return', 'if (false) return', P.pol], ['actionHash-ul plicului nu mai e canonic', 'agent-policy.mjs', "actionHash: sha256(Buffer.from(canonical(action), 'utf8')),", "actionHash: sha256(Buffer.from(JSON.stringify(action), 'utf8')),", P.pol], ]; const FISIERE = ['agent-policy.mjs', 'agent-ledger.mjs', 'agent-aprobare.mjs', 'agent-cli.mjs', P.pol, P.reg, P.apr, P.cli]; function copie() { const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b2-ctl-')); for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, f)); return t; } +// 2026-09-29: probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun +const PARALEL = 4; function ruleaza(t, proba) { const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF; - const r = spawnSync(process.execPath, [path.join(t, proba)], { encoding: 'utf8', timeout: 240000, env }); - const out = (r.stdout || '') + (r.stderr || ''); - const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out); - return { cod: r.status, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length }; + return new Promise((resolve) => { + const c = spawn(process.execPath, [path.join(t, proba)], { env }); let out = ''; + const ceas = setTimeout(() => c.kill(), 240000); + c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + c.on('close', (cod) => { clearTimeout(ceas); const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out); + resolve({ cod, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length }); }); + }); } +async function inGrup(lucrari) { const rez = new Array(lucrari.length); let i = 0; + await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < lucrari.length) { const k = i++; rez[k] = await lucrari[k](); } })); return rez; } let esecuri = 0; -for (const proba of Object.values(P)) { - const t0 = copie(); const m0 = ruleaza(t0, proba); fs.rmSync(t0, { recursive: true, force: true }); +const martori = await inGrup(Object.values(P).map((proba) => async () => { const t0 = copie(); try { return [proba, await ruleaza(t0, proba)]; } finally { fs.rmSync(t0, { recursive: true, force: true }); } })); +for (const [proba, m0] of martori) { const verde = m0.rulat && m0.rele === 0 && (m0.cod === 0 || (m0.cod === 2 && proba === P.pol && !VERIFY)); if (verde) console.log(` OK martorul ${proba}: copia neatinsa verde${m0.cod === 2 ? ' (fara verificatorul AIP-23: partea lui NEMASURATA)' : ''}`); else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rele + ' RAU' : 'nu a ajuns la rezumat'})`); } } -for (const [nume, f, din, inl, proba] of PLANTARI) { - const t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8'); - if (src.split(din).length !== 2) { esecuri++; console.log(` RAU ${nume}: tiparul nu apare exact o data in ${f}`); fs.rmSync(t, { recursive: true, force: true }); continue; } - fs.writeFileSync(fp, src.replace(din, inl)); - const r = ruleaza(t, proba); fs.rmSync(t, { recursive: true, force: true }); - if (r.rulat && r.cod !== 0 && r.rele > 0) console.log(` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`); - else { esecuri++; console.log(` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`); } -} +const linii = await inGrup(PLANTARI.map((pl) => async () => { + const [nume, f, din, inl, proba] = pl; let t = null; + try { + // o plantare scrisa gresit (fisier, tipar) e un esec al controlului, nu o cadere a lui + if (!FISIERE.includes(f) || typeof din !== 'string' || !proba) return [false, ` RAU ${nume}: plantarea e scrisa gresit (fisier ${f}, proba ${proba})`]; + t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8'); + if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul nu apare exact o data in ${f}`]; + fs.writeFileSync(fp, src.replace(din, inl)); + const r = await ruleaza(t, proba); + if (r.rulat && r.cod !== 0 && r.rele > 0) return [true, ` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`]; + return [false, ` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`]; + } catch (e) { return [false, ` RAU ${nume}: controlul a cazut pe ea (${String(e.message).slice(0, 80)})`]; } + finally { if (t) fs.rmSync(t, { recursive: true, force: true }); } +})); +for (const [bun, l] of linii) { console.log(l); if (!bun) esecuri++; } console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii`); process.exitCode = esecuri ? 1 : 0; diff --git a/agents/proba-agent-policy.mjs b/agents/proba-agent-policy.mjs index 5a05d16..6a899e7 100644 --- a/agents/proba-agent-policy.mjs +++ b/agents/proba-agent-policy.mjs @@ -38,6 +38,15 @@ try { const altActiv = checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '10', asset: 'USDC', at: 1 }); cer(`CONTROL: plata in alt activ decat al limitei -> denied (${altActiv.reason})`, !altActiv.allowed && /not the policy's asset/.test(altActiv.reason)); + // portofelul numit (2026-09-29): o plata trebuie sa spuna `from` si sa fie chiar el + const cuPortofel = definePolicy({ agentId: 'agent-1', spend: { amount: '100', windowSeconds: 3600 }, wallet: '0xAbCd00000000000000000000000000000000Ef01' }).policy; + cer('portofelul politicii e scris cu litere mici', cuPortofel.wallet === '0xabcd00000000000000000000000000000000ef01'); + cer('plata din portofelul numit (orice litere) -> allowed', checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0xABCD00000000000000000000000000000000EF01', at: 1 }).allowed === true); + const fara = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', at: 1 }); + const dinAlt = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0x1111111111111111111111111111111111111111', at: 1 }); + cer(`CONTROL: plata fara from, sau din alt portofel, sub o politica ce numeste portofelul -> denied (${dinAlt.reason})`, !fara.allowed && !dinAlt.allowed && /not from the wallet/.test(dinAlt.reason)); + cer('CONTROL: un portofel care nu e adresa EVM -> politica refuzata', /wallet must be an EVM address/.test(arunca(() => definePolicy({ agentId: 'a', wallet: '0x12' })) || '')); + // validarea politicii cer('CONTROL: spend.amount care nu e intreg -> politica refuzata', /spend.amount/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1e9', windowSeconds: 60 } })) || '')); cer('CONTROL: fereastra zero -> politica refuzata', /windowSeconds/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1', windowSeconds: 0 } })) || '')); diff --git a/agents/x402/README.md b/agents/x402/README.md new file mode 100644 index 0000000..949cc38 --- /dev/null +++ b/agents/x402/README.md @@ -0,0 +1,76 @@ +# Agent payments over x402: a wallet that pays only what the agent's policy allows + +An AI agent that holds a payment key can spend without limit, whatever its policy says: the policy is only a promise. Here the +agent does **not** hold the payment key. A small service, the **agent wallet**, holds it for the owner, and signs an x402 payment only +when the agent has written that payment into its signed ledger (`../agent-ledger.mjs`) and the owner's policy allows it. The agent +authenticates to the wallet with the ledger entry itself, which only its ML-DSA-65 key can sign. + +Node.js 24 and `ethers` (for EIP-712 and secp256k1: `npm install` here). Payments follow x402 version 2 (`exact` scheme, EIP-3009 +`transferWithAuthorization`), with the HTTP headers `PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE` and `PAYMENT-RESPONSE`. + +## The flow + +1. The agent asks for a resource and gets `402` with the payment requirements. +2. The agent writes the payment into its ledger: `{ kind: "payment", from: , to: payTo, amount, asset: "eip155:/erc20:", + ref: }`. If its policy refuses (over the limit, a recipient not allowed, approval + missing, revoked), it stops here. +3. The agent sends the requirement and its ledger to the wallet (`POST /authorize`). The wallet: + - checks the requirement (its network and asset, a positive integer amount, a timeout it caps at 300 seconds); + - verifies the whole ledger without trusting the agent, under the policy the owner pinned when starting the wallet: identity, + every signature, the chain of hashes, every decision re-run, the revocations the owner gave the wallet, and the **heads the + wallet itself saw before**, with its own clock. A ledger that does not continue what the wallet saw is a branch: it is refused + and, when the wallet kept the entry, answered with a proof of equivocation anyone can check; + - requires the last entry to be a new, allowed payment from this wallet, naming exactly this purchase, written now by the + wallet's clock (within 120 seconds); + - checks the limit a second time against what the **wallet itself has signed**, so a rewritten ledger, a branch, or a new ledger + under a changed policy cannot take out more than the limit allows; + - signs an EIP-3009 authorization whose nonce is `sha256(entry hash)`: the on-chain payment names the ledger entry, and one entry + can pay once, at the wallet and on chain. +4. The agent retries the resource with `PAYMENT-SIGNATURE`; the resource server settles through its facilitator and serves. + +`verifica-plati.mjs` then proves from the outside that a wallet's on-chain payments were allowed by the agent's policy: the ledger +verifies, each payment names an allowed entry, its transaction emits `AuthorizationUsed(wallet, sha256(entry hash))` and the +`Transfer` of that amount, and with `--all-transfers` every transfer out of the wallet since a block is one of those payments. + +## Use + +``` +node wallet.mjs serve --config wallet.json [--port 8793] # POST /authorize, POST /revocations, GET /status on 127.0.0.1 +``` + +`wallet.json`: `{ policy, policyHash, network: "eip155:28001", token: { address, name, version }, evmKeyFile, stateFile }`. The +policy must name the wallet (`wallet: `, see `definePolicy`) and a spending limit in its asset, or the wallet does not +start. The EVM key is read from its file and never printed; the state file keeps the heads seen and what was signed, and is written +before a signature is returned. The agent side is `payWithAgent({ url, ledger, wallet })` in `client.mjs`; `walletOverHttp(url)` +talks to the service. `resource-server.mjs` is a minimal x402 seller for tests; `facilitator-local.mjs` makes the same checks as an +x402 facilitator, in memory, for tests without a chain. + +## What it does not do + +- It is not trustless custody: whoever holds the wallet key (the owner) can pay without the agent. A 2-of-2 contract wallet + (the agent and the policy service, ERC-1271, which the testnet token accepts) would remove that trust; it is not built. +- An authorization that is signed but never settled still counts against the limit until the window passes (conservative). +- The wallet API must be reached over a trusted channel: an entry intercepted on the way could be submitted by someone else, who + would then receive the payment payload (the money still goes to the seller the entry names). +- It does not check that the seller delivered what was paid for. +- The wallet keeps every signed payment in its state; nothing prunes it yet. + +## Tests + +Measured on 2026-09-29 (Node.js 24.14.1, ethers 6.16.0): + +| test | result | +|---|---| +| `node proba-wallet.mjs` | 25/25 without a network, real EIP-712 signatures and ML-DSA-65 keys: five paid purchases through a local facilitator, the sixth refused by the policy; attacks by the holder of the agent key (a forged "allowed" entry over the limit, an entry for another amount, the same entry twice, a branch with its equivocation proof, a new ledger under a changed policy, a backdated entry, two branches sent at the same time), the owner's revocation, human approval, a payment for another requirement and a replayed payment at the seller | +| `node proba-verifica-plati.mjs` | 10/10 without a network, on the chain responses recorded for the testnet run below | +| `node control-negativ-wallet.mjs` | 21/21: each guard of the wallet, the client, the seller and the verifier removed in a copy, and the named check must fail | +| `AERE_TESTNET_KEY_FILE= node proba-x402-testnet.mjs` | 9/9 on the public testnet 28001 through its x402 facilitator: a fresh wallet funded with 0.05 tUSD, three purchases of 0.01 paid and settled on chain, the fourth refused by the policy, a replayed payment refused, the balances and the used authorization nonces read on chain, and the evidence file verified `VALID` (a changed amount, or a payment removed from the file: `INVALID`) | + +The testnet run of 2026-09-29 is in `dovezi-28001/`: the evidence file and the recorded chain responses. Anyone can check it: + +``` +node verifica-plati.mjs dovezi-28001/plati-agent-2026-09-29-20-23-16.json --rpc https://testnet-rpc.aere.network --all-transfers +``` + +tUSD is the testnet's EIP-3009 token, minted by a faucet and worth nothing. Nothing here has been run on the Aere Network mainnet. +Code comments and test names are in Romanian; messages, data and the documentation are in English. No third party has reviewed it. diff --git a/agents/x402/client.mjs b/agents/x402/client.mjs new file mode 100644 index 0000000..a7ddb23 --- /dev/null +++ b/agents/x402/client.mjs @@ -0,0 +1,48 @@ +// Clientul x402 al unui agent AI (2026-09-29, punctele 23, 25): cumpara o resursa HTTP care cere plata (402), scriind INTAI plata in +// registrul agentului, apoi cerand portofelului (wallet.mjs) semnatura, apoi reluand cererea cu antetul PAYMENT-SIGNATURE. Numele +// antetelor si forma lor sunt ale specificatiei x402 v2 (transportul HTTP): PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE, toate +// JSON in base64. Daca politica refuza plata, clientul se opreste inainte de portofel (si portofelul ar refuza oricum). +import { x402Action } from './wallet.mjs'; + +export const b64json = (o) => Buffer.from(JSON.stringify(o), 'utf8').toString('base64'); +export function dinB64json(s) { try { return JSON.parse(Buffer.from(String(s), 'base64').toString('utf8')); } catch { return null; } } + +/** portofelul prin HTTP (serviciul wallet.mjs serve), cu aceeasi forma ca obiectul din createWallet */ +export function walletOverHttp(baseUrl, fetchImpl = fetch) { + const b = String(baseUrl).replace(/\/+$/, ''); + return { + async status() { const r = await fetchImpl(b + '/status'); return r.json(); }, + async authorize(body) { const r = await fetchImpl(b + '/authorize', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); return r.json(); }, + }; +} + +/** + * @param {object} o + * @param {string} o.url resursa + * @param {object} o.ledger registrul agentului (openLedger / resumeLedger) + * @param {object} o.wallet portofelul (createWallet sau walletOverHttp) + * @param {Array} [o.approvals] aprobari umane pentru aceasta plata, cand politica le cere + * @param {Function} [o.fetchImpl] + * @returns {Promise<{paid:boolean, status:number, reason?:string, body?:string, settlement?:object, entry?:object, receipt?:object, decision?:object}>} + */ +export async function payWithAgent({ url, ledger, wallet, approvals = [], fetchImpl = fetch }) { + const r0 = await fetchImpl(url); + if (r0.status !== 402) return { paid: false, status: r0.status, body: await r0.text(), reason: r0.ok ? 'no payment was required' : `the resource answered ${r0.status}` }; + const pr = dinB64json(r0.headers.get('payment-required')) || (await r0.json().catch(() => null)); + if (!pr || pr.x402Version !== 2 || !Array.isArray(pr.accepts)) return { paid: false, status: 402, reason: 'the 402 response carries no x402 v2 PaymentRequired' }; + const w = await wallet.status(); + const req = pr.accepts.find((a) => a.scheme === 'exact' && a.network === w.network && String(a.asset).toLowerCase() === String(w.asset).toLowerCase()); + if (!req) return { paid: false, status: 402, reason: `no accepted payment is in this wallet's asset on ${w.network}` }; + // 1. plata, in registru, inainte de orice semnatura + const r = ledger.record(x402Action(w.address, pr.resource || null, req), { approvals }); + if (!r.allowed) return { paid: false, status: 402, reason: `the agent's policy refused the payment: ${r.reason}`, entry: r.entry }; + // 2. semnatura portofelului, care judeca din nou tot registrul + const a = await wallet.authorize({ requirements: req, resource: pr.resource || null, ledger: ledger.export() }); + if (!a.ok) return { paid: false, status: 402, reason: `the wallet refused: ${a.error}`, entry: r.entry, equivocation: a.equivocation }; + // 3. cererea reluata, cu plata + const r1 = await fetchImpl(url, { headers: { 'PAYMENT-SIGNATURE': a.header } }); + const settlement = dinB64json(r1.headers.get('payment-response')); + const body = await r1.text(); + return { paid: r1.ok && !!(settlement && settlement.success), status: r1.status, body, settlement, entry: r.entry, receipt: a.receipt, decision: a.decision, + ...(r1.ok ? {} : { reason: `the resource answered ${r1.status} after payment: ${body.slice(0, 160)}` }) }; +} diff --git a/agents/x402/control-negativ-wallet.mjs b/agents/x402/control-negativ-wallet.mjs new file mode 100644 index 0000000..8af725b --- /dev/null +++ b/agents/x402/control-negativ-wallet.mjs @@ -0,0 +1,88 @@ +// Controlul negativ al portofelului de plati x402 (proba-wallet.mjs) si al verificatorului de plati (proba-verifica-plati.mjs): fiecare paznic se strica intr-o COPIE (agent-policy/*.mjs si +// x402/*.mjs, in aceeasi asezare), proba ruleaza pe copie si trebuie sa iasa rosie EXACT pe verificarea numita, cu proba chiar rulata. +// Plantarile sunt conditii false la rulare sau randuri scoase, deci copia se incarca intotdeauna; o ancora care nu apare exact o data, +// sau o proba care nu ajunge la rezumat, e un esec al controlului (STRICAT), nu o linie informativa. +// node control-negativ-wallet.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const SUS = path.resolve(AICI, '..'); +// ethers se rezolva de langa original (copia nu are node_modules); calea se da copiei prin AERE_ETHERS +let ETHERS = process.env.AERE_ETHERS || null; // o copie a acestui dosar (controlul portii) primeste calea din mediu +if (!ETHERS) try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve('ethers'); } +catch { try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve(path.resolve(SUS, '..', '..', 'contracts', 'node_modules', 'ethers')); } catch { console.log('NEMASURAT: ethers nu se gaseste (npm install aici, sau AERE_ETHERS=)'); process.exit(2); } } +const V = 'proba-verifica-plati.mjs'; +const PLANTARI = [ + // [nume, fisier (relativ la x402/), tipar, inlocuire, textul verificarii care trebuie sa iasa rosie] + ['cererile nu mai sunt judecate una cate una', 'wallet.mjs', 'function authorize(x) { const r = coada.then(() => autorizeaza(x)); coada = r.catch(() => {}); return r; }', 'function authorize(x) { return autorizeaza(x); }', '12. ATAC'], + ['registrul nu mai trebuie sa verifice', 'wallet.mjs', ' if (!v.ok) {', " if (!v.ok && process.env.AERE_PLANTA_NICIODATA === 'da') {", '3. ATAC'], + ['intrarea nu mai trebuie sa numeasca aceasta cumparatura', 'wallet.mjs', "for (const k of ['from', 'to', 'amount', 'asset', 'ref']) if (", 'for (const k of []) if (', '4. ATAC'], + ['aceeasi intrare poate plati de doua ori', 'wallet.mjs', 'if (S.last && e.seq <= S.last.seq) return refuz(', "if (S.last && e.seq <= S.last.seq && process.env.AERE_PLANTA_NICIODATA === 'da') return refuz(", '5. ATAC'], + ['intrarea nu mai trebuie sa fie de acum', 'wallet.mjs', 'if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5)) return', "if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5) && process.env.AERE_PLANTA_NICIODATA === 'da') return", '10. ATAC'], + ['limita nu mai e judecata fata de ce a semnat portofelul', 'wallet.mjs', 'stare.signed.map((s) => ({ amount: s.amount, at: s.at }))', '[]', '7. politica noua'], + ['aprobarile intrarii nu mai sunt date limitei portofelului', 'wallet.mjs', 'if (r.ok) aprobatori.push(r.humanId); }', '}', '9. 30000'], + ['dovada de echivocare nu mai e data', 'wallet.mjs', 'if (p.agentId === policy.agentId && verifyEquivocation(p).ok) r.equivocation = p;', '', '6. si portofelul da dovada'], + ['nonce-ul autorizarii nu mai numeste intrarea', 'wallet.mjs', 'nonce: nonceForEntry(e.hash) };', "nonce: '0x' + crypto.randomBytes(32).toString('hex') };", '1. nonce-ul fiecarei'], + ['revocarile primite nu mai ajung la verificarea registrului', 'wallet.mjs', 'revocations: stare.revocations, anchors', 'revocations: [], anchors', '8. dupa revocare'], + ['serverul de resurse primeste o plata pentru alta cerinta', 'resource-server.mjs', 'if (cheie(payload.accepted) !== cheie(requirement)) return cere(', "if (cheie(payload.accepted) !== cheie(requirement) && process.env.AERE_PLANTA_NICIODATA === 'da') return cere(", '11. CONTROL'], + ['clientul cere semnatura si cand politica a refuzat', 'client.mjs', "if (!r.allowed) return { paid: false, status: 402, reason: `the agent's policy refused the payment: ${r.reason}`, entry: r.entry };", '', '2. CONTROL'], + ['portofelul porneste si fara limita in activul lui', 'wallet.mjs', 'if (!policy.spend || policy.spend.asset !== assetId(network, token)) throw', "if ((!policy.spend || policy.spend.asset !== assetId(network, token)) && process.env.AERE_PLANTA_NICIODATA === 'da') throw", 'o politica fara limita'], + // verificatorul platilor, pe raspunsurile inregistrate de pe 28001 + ['verificatorul nu mai cere ca registrul sa verifice', 'verifica-plati.mjs', 'entries)`, v.ok, v.error', 'entries)`, true, v.error', '2. CONTROL: o suma schimbata', V], + ['verificatorul nu mai cere ca intrarea platii sa fie in registru', 'verifica-plati.mjs', '!!e && e.hash === p.entryHash)', '!!e)', '3. CONTROL', V], + ['verificatorul nu mai cere portofelul in intrare', 'verifica-plati.mjs', "String(a.from).toLowerCase() === wallet && ", '', '4. CONTROL', V], + ['verificatorul nu mai cere status 1', 'verifica-plati.mjs', "!!rc && rc.status === '0x1'", '!!rc', '5. CONTROL', V], + ['verificatorul nu mai cere nonce-ul intrarii pe lant', 'verifica-plati.mjs', '&& l.topics[2].toLowerCase() === nonce));', '));', '6. CONTROL', V], + ['verificatorul nu mai cere suma din Transfer', 'verifica-plati.mjs', '&& BigInt(l.data) === BigInt(a.amount)));', '));', '7. CONTROL', V], + ['verificatorul nu mai cere ca orice Transfer sa fie in registru', 'verifica-plati.mjs', 'straine.length === 0,', 'true,', '8. CONTROL', V], + ['verificatorul nu mai cere lantul dosarului', 'verifica-plati.mjs', 'lantul === chain,', 'true,', '9. CONTROL', V], +]; +function copie() { + const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-wallet-ctl-')); + for (const f of fs.readdirSync(SUS).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(SUS, f), path.join(t, f)); + fs.mkdirSync(path.join(t, 'x402')); + for (const f of fs.readdirSync(AICI).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(AICI, f), path.join(t, 'x402', f)); + fs.cpSync(path.join(AICI, 'dovezi-28001'), path.join(t, 'x402', 'dovezi-28001'), { recursive: true }); + return t; +} +// probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun +const PARALEL = 4; +function ruleaza(t, proba = 'proba-wallet.mjs') { + return new Promise((resolve) => { + const c = spawn(process.execPath, [path.join(t, 'x402', proba)], { env: { ...process.env, AERE_ETHERS: ETHERS } }); let out = ''; + const ceas = setTimeout(() => c.kill(), 240000); + c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /(agent-wallet|verifica-plati): \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => /^\s+RAU\s/.test(l)) }); }); + }); +} +async function inGrup(lucrari) { const rez = new Array(lucrari.length); let i = 0; + await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < lucrari.length) { const k = i++; rez[k] = await lucrari[k](); } })); return rez; } +let esecuri = 0; +const martori = await inGrup(['proba-wallet.mjs', V].map((proba) => async () => { const t0 = copie(); try { return [proba, await ruleaza(t0, proba)]; } finally { fs.rmSync(t0, { recursive: true, force: true }); } })); +for (const [proba, m0] of martori) { + if (m0.cod === 0 && m0.rulat && !m0.rosii.length) console.log(` OK martorul ${proba}: copia neatinsa verde`); + else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rosii.length + ' RAU' : 'nu a ajuns la rezumat'})`); } +} +const linii = await inGrup(PLANTARI.map(([nume, f, din, inl, tinta, proba = 'proba-wallet.mjs']) => async () => { + let t = null; + try { + t = copie(); const fp = path.join(t, 'x402', f); + if (!fs.existsSync(fp)) return [false, ` RAU ${nume}: plantarea numeste un fisier care nu exista (${f})`]; + const src = fs.readFileSync(fp, 'utf8'); + if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${f} (STRICAT)`]; + fs.writeFileSync(fp, src.replace(din, inl)); + const r = await ruleaza(t, proba); + if (!r.rulat) return [false, ` RAU ${nume}: proba nu a ajuns la rezumat (STRICAT, cod ${r.cod})`]; + if (r.cod !== 0 && r.rosii.some((l) => l.includes(tinta))) return [true, ` OK ${nume}: '${tinta}' ROSIE (${r.rosii.length} RAU)`]; + return [false, ` RAU ${nume}: '${tinta}' a ramas verde (${r.rosii.length} RAU altundeva)`]; + } catch (e) { return [false, ` RAU ${nume}: controlul a cazut pe ea (${String(e.message).slice(0, 80)})`]; } + finally { if (t) fs.rmSync(t, { recursive: true, force: true }); } +})); +for (const [bun, l] of linii) { console.log(l); if (!bun) esecuri++; } +console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii pe verificarea lor`); +process.exitCode = esecuri ? 1 : 0; diff --git a/agents/x402/dovezi-28001/plati-agent-2026-09-29-20-23-16.json b/agents/x402/dovezi-28001/plati-agent-2026-09-29-20-23-16.json new file mode 100644 index 0000000..fe41684 --- /dev/null +++ b/agents/x402/dovezi-28001/plati-agent-2026-09-29-20-23-16.json @@ -0,0 +1,164 @@ +{ + "v": 1, + "kind": "aere-agent-x402-payments", + "network": "eip155:28001", + "token": "0x8215bA247a3574af8EBC36606eB437811E318FBd", + "wallet": "0xe61F2aC98f18465071AfD3F2E9A39eE1b2a564E9", + "fromBlock": 4022094, + "facilitator": "https://testnet-rpc.aere.network/x402", + "policy": { + "v": 1, + "kind": "aere-agent-policy", + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "spend": { + "amount": "30000", + "windowSeconds": 3600, + "asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd" + }, + "tools": null, + "recipients": [ + "0x5b63415ab4b96d55987c39a00f22a12f2732edc3" + ], + "wallet": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9" + }, + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "ledger": { + "version": "aere-agent-ledger/2 (2026-09-29)", + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n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\npHDKIeTs\n-----END PUBLIC KEY-----\n", + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "session": "c6ff3295bd5220f2f63b8d22f303ec71", + "entries": [ + { + "seq": 0, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "body": { + "v": 2, + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "session": "c6ff3295bd5220f2f63b8d22f303ec71", + "at": 1790713379, + "action": { + "kind": "payment", + "from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3", + "amount": "10000", + "asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd", + "ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab", + "at": 1790713379 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 0 + }, + "signature": "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", + "hash": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031" + }, + { + "seq": 1, + "prev": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031", + "body": { + "v": 2, + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "session": "c6ff3295bd5220f2f63b8d22f303ec71", + "at": 1790713384, + "action": { + "kind": "payment", + "from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3", + "amount": "10000", + "asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd", + "ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab", + "at": 1790713384 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 1 + }, + "signature": "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", + "hash": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f" + }, + { + "seq": 2, + "prev": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f", + "body": { + "v": 2, + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "session": "c6ff3295bd5220f2f63b8d22f303ec71", + "at": 1790713389, + "action": { + "kind": "payment", + "from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3", + "amount": "10000", + "asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd", + "ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab", + "at": 1790713389 + }, + "decision": { + "allowed": true, + "reason": "under the limit" + }, + "provenance": null, + "seq": 2 + }, + "signature": "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", + "hash": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a" + }, + { + "seq": 3, + "prev": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a", + "body": { + "v": 2, + "agentId": "aere-agent:aa421db05d854123cffaa8bb8f4a4b48f59ed58c", + "policyHash": "0xa5cf38c4fd450def38fbd553d5fe6e7e012d23d9e99273476a73ee8292ba896d", + "session": "c6ff3295bd5220f2f63b8d22f303ec71", + "at": 1790713393, + "action": { + "kind": "payment", + "from": "0xe61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "to": "0x5b63415ab4b96d55987c39a00f22a12f2732edc3", + "amount": "10000", + "asset": "eip155:28001/erc20:0x8215ba247a3574af8ebc36606eb437811e318fbd", + "ref": "0x5298562c9b9ecccf42306cc056d2f0b325efabf15789042fe1a4899fdd8de0ab", + "at": 1790713393 + }, + "decision": { + "allowed": false, + "reason": "over the limit: 40000 > 30000 per 3600s" + }, + "provenance": null, + "seq": 3 + }, + "signature": "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", + "hash": "b53c460c2d5aa8958a1aaf272a8fd1cb2cb6a1d33b18c6b3caa9177c5a835cef" + } + ] + }, + "payments": [ + { + "entrySeq": 0, + "entryHash": "242009507a0fd0b54edeb9ad783884700d895d1982846ee4ef58c4246a29d031", + "transaction": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e" + }, + { + "entrySeq": 1, + "entryHash": "5099803b654458f65044420af98b1a9ddb1aeab6956f318d9123b85b504edf2f", + "transaction": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9" + }, + { + "entrySeq": 2, + "entryHash": "647151d57f48ad4629d8e943969b7fa80a9d7e835823b2cd3da0430460e03d0a", + "transaction": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63" + } + ], + "createdAt": "2026-09-29T20:23:16.661Z" +} diff --git a/agents/x402/dovezi-28001/rpc-inregistrat-2026-09-29-20-23-16.json b/agents/x402/dovezi-28001/rpc-inregistrat-2026-09-29-20-23-16.json new file mode 100644 index 0000000..3ccdb79 --- /dev/null +++ b/agents/x402/dovezi-28001/rpc-inregistrat-2026-09-29-20-23-16.json @@ -0,0 +1,205 @@ +{ + "v": 1, + "recordedAt": "2026-09-29T20:23:48.503Z", + "rpc": "https://testnet-rpc.aere.network", + "chainId": "0x6d61", + "receipts": { + "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e": { + "blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab", + "blockNumber": "0x3d5f58", + "contractAddress": null, + "cumulativeGasUsed": "0x144c4", + "from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48", + "gasUsed": "0x144c4", + "effectiveGasPrice": "0x3b9aca00", + "logs": [ + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x56f33b19d8186232d0a9f9d6c049389d88e0ab7d4f81eb9bd00ffc98b6605a48" + ], + "data": "0x", + "blockNumber": "0x3d5f58", + "transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e", + "transactionIndex": "0x0", + "blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab", + "blockTimestamp": "0x6abc1e24", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "blockNumber": "0x3d5f58", + "transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e", + "transactionIndex": "0x0", + "blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab", + "blockTimestamp": "0x6abc1e24", + "logIndex": "0x1", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000008000000000000000000000080000000000000000000000000000080000000000000000000000000000000000000000050000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000040000010000000004000000000000000000000000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000080000000000000002000000000000000000000000020", + "status": "0x1", + "to": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e", + "transactionIndex": "0x0", + "type": "0x2" + }, + "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9": { + "blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a", + "blockNumber": "0x3d5f61", + "contractAddress": null, + "cumulativeGasUsed": "0x101f8", + "from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48", + "gasUsed": "0x101f8", + "effectiveGasPrice": "0x3b9aca00", + "logs": [ + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x65a47f30ec24141e06c9c82740601a3099f596a3c01c7bb80d3351753f8d8818" + ], + "data": "0x", + "blockNumber": "0x3d5f61", + "transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9", + "transactionIndex": "0x0", + "blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a", + "blockTimestamp": "0x6abc1e29", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "blockNumber": "0x3d5f61", + "transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9", + "transactionIndex": "0x0", + "blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a", + "blockTimestamp": "0x6abc1e29", + "logIndex": "0x1", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000009000000000000000000000080000000000000000000000000000080000000000000004000000000000000000000000010000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000010000000004000000000000000000000000000000000002000002000000000000000400000000000000000000000000000000000000000000000000000080000000000000002000000000000000000000000000", + "status": "0x1", + "to": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9", + "transactionIndex": "0x0", + "type": "0x2" + }, + "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63": { + "blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b", + "blockNumber": "0x3d5f69", + "contractAddress": null, + "cumulativeGasUsed": "0x101ec", + "from": "0x50f2a153be2c248b7050914a08f4f6f4498c4e48", + "gasUsed": "0x101ec", + "effectiveGasPrice": "0x3b9aca00", + "logs": [ + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x1ae82892234488ec0d6e8f19a324a50682f0c7afccf2ec0e1e66a9cce1a5f443" + ], + "data": "0x", + "blockNumber": "0x3d5f69", + "transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63", + "transactionIndex": "0x0", + "blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b", + "blockTimestamp": "0x6abc1e2d", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "blockNumber": "0x3d5f69", + "transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63", + "transactionIndex": "0x0", + "blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b", + "blockTimestamp": "0x6abc1e2d", + "logIndex": "0x1", + "removed": false + } + ], + "logsBloom": "0x00000000000000002000000000000000000000000000000008000000000000000000000000000000000020000000000000000000000000000000000200000000000000008000000000000008000000000000000000000080000000000000000000000000000080000000000000000000000000000000000000000010000000000000002000000000000000000000000000000000000000004000000000000000000000000000000000000000010000000004000000000000000000000000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000080000000000000002010000000000000000000000000", + "status": "0x1", + "to": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63", + "transactionIndex": "0x0", + "type": "0x2" + } + }, + "transfersOut": [ + { + "logIndex": "0x1", + "removed": false, + "blockNumber": "0x3d5f58", + "blockHash": "0x57b530673634ee3454a25cf2fef87ae019bdb7f7dc29041309f7248e5bb637ab", + "blockTimestamp": "0x6abc1e24", + "transactionHash": "0xd70d44785359419a460226d3c54e8f76375332a5cf8634421b04e30b47e4c16e", + "transactionIndex": "0x0", + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ] + }, + { + "logIndex": "0x1", + "removed": false, + "blockNumber": "0x3d5f61", + "blockHash": "0xc91066be5b1ddfa9cfe7a507f8d8279a7745ab4733aab012eac76909b91c4f6a", + "blockTimestamp": "0x6abc1e29", + "transactionHash": "0xb9ece53b4f2106de68321c41d22ec1f37b9a1b38cce46747dca496de913ebdb9", + "transactionIndex": "0x0", + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ] + }, + { + "logIndex": "0x1", + "removed": false, + "blockNumber": "0x3d5f69", + "blockHash": "0x8ff8184bb076ad26e8b4e637ffdc596c63ba89f6e7eb94c53d300dd55a93a99b", + "blockTimestamp": "0x6abc1e2d", + "transactionHash": "0x02bb4b86da192c8277b36297a39233dd7b8831e6125385c32dc5f009908ecb63", + "transactionIndex": "0x0", + "address": "0x8215ba247a3574af8ebc36606eb437811e318fbd", + "data": "0x0000000000000000000000000000000000000000000000000000000000002710", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000e61f2ac98f18465071afd3f2e9a39ee1b2a564e9", + "0x0000000000000000000000005b63415ab4b96d55987c39a00f22a12f2732edc3" + ] + } + ] +} diff --git a/agents/x402/facilitator-local.mjs b/agents/x402/facilitator-local.mjs new file mode 100644 index 0000000..976d57d --- /dev/null +++ b/agents/x402/facilitator-local.mjs @@ -0,0 +1,53 @@ +// Un facilitator x402 v2 LOCAL, pentru probe fara retea (2026-09-29): aceleasi verificari ca facilitatorul de pe testnetul 28001 +// (x402/facilitator/facilitator.mjs din depozitul de dezvoltare: forma, reteaua, activul, payTo, suma, fereastra, semnatura EIP-712 +// recuperata, nonce nefolosit, soldul), dar soldurile si nonce-urile folosite stau in memorie, iar /settle le muta in memorie in loc +// sa trimita o tranzactie. Nu e un facilitator de folosit in productie: nu atinge niciun lant. +import http from 'node:http'; +import { incarcaEthers, EIP3009_TYPES } from './wallet.mjs'; + +export function createLocalFacilitator({ network, token, balances = {} }) { + const ethers = incarcaEthers(); + const domeniu = { name: token.name, version: token.version, chainId: Number(network.split(':')[1]), verifyingContract: ethers.getAddress(token.address) }; + const sold = new Map(Object.entries(balances).map(([a, v]) => [a.toLowerCase(), BigInt(v)])); + const folosite = new Set(); + function verifica(body) { + const pp = body && body.paymentPayload, pr = body && body.paymentRequirements; + if (!pp || !pr || body.x402Version !== 2 || pp.x402Version !== 2) return { ok: false, reason: 'invalid_payload' }; + if (pr.scheme !== 'exact' || pr.network !== network) return { ok: false, reason: 'unsupported_network' }; + if (String(pr.asset).toLowerCase() !== token.address.toLowerCase()) return { ok: false, reason: 'unsupported_asset' }; + const a = pp.payload && pp.payload.authorization, sig = pp.payload && pp.payload.signature; + if (!a || !sig) return { ok: false, reason: 'invalid_payload' }; + if (String(a.to).toLowerCase() !== String(pr.payTo).toLowerCase()) return { ok: false, reason: 'invalid_payment_requirements', payer: a.from }; + if (BigInt(a.value) < BigInt(pr.amount)) return { ok: false, reason: 'insufficient_amount', payer: a.from }; + const acum = BigInt(Math.floor(Date.now() / 1000)); + if (acum <= BigInt(a.validAfter)) return { ok: false, reason: 'authorization_not_yet_valid', payer: a.from }; + if (acum + 6n >= BigInt(a.validBefore)) return { ok: false, reason: 'authorization_expired', payer: a.from }; + let semnatar = null; + try { semnatar = ethers.verifyTypedData(domeniu, EIP3009_TYPES, { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) }, sig); } catch { semnatar = null; } + if (!semnatar || semnatar.toLowerCase() !== String(a.from).toLowerCase()) return { ok: false, reason: 'invalid_signature', payer: a.from }; + if (folosite.has(`${a.from.toLowerCase()}:${a.nonce}`)) return { ok: false, reason: 'nonce_already_used', payer: a.from }; + if ((sold.get(a.from.toLowerCase()) || 0n) < BigInt(a.value)) return { ok: false, reason: 'insufficient_funds', payer: a.from }; + return { ok: true, payer: a.from, a }; + } + let n = 0; + const srv = http.createServer((req, res) => { + let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => { + const trimite = (o) => { const b = JSON.stringify(o); res.writeHead(200, { 'content-type': 'application/json' }); res.end(b); }; + let body; try { body = JSON.parse(s || 'null'); } catch { body = null; } + const url = (req.url || '').split('?')[0]; + const v = verifica(body); + if (url === '/verify') return trimite(v.ok ? { isValid: true, payer: v.payer } : { isValid: false, invalidReason: v.reason, payer: v.payer || '' }); + if (url === '/settle') { + if (!v.ok) return trimite({ success: false, errorReason: v.reason, payer: v.payer || '', transaction: '', network }); + const a = v.a; folosite.add(`${a.from.toLowerCase()}:${a.nonce}`); + sold.set(a.from.toLowerCase(), sold.get(a.from.toLowerCase()) - BigInt(a.value)); + sold.set(a.to.toLowerCase(), (sold.get(a.to.toLowerCase()) || 0n) + BigInt(a.value)); + return trimite({ success: true, payer: a.from, transaction: '0x' + (++n).toString(16).padStart(64, '0'), network }); + } + res.writeHead(404); res.end(); + }); + }); + return { server: srv, balanceOf: (a) => sold.get(String(a).toLowerCase()) || 0n, used: folosite, + fund: (a, v) => sold.set(String(a).toLowerCase(), (sold.get(String(a).toLowerCase()) || 0n) + BigInt(v)), + listen: () => new Promise((r) => srv.listen(0, '127.0.0.1', () => r(`http://127.0.0.1:${srv.address().port}`))), close: () => srv.close() }; +} diff --git a/agents/x402/inregistreaza-rpc.mjs b/agents/x402/inregistreaza-rpc.mjs new file mode 100644 index 0000000..b139ad6 --- /dev/null +++ b/agents/x402/inregistreaza-rpc.mjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// inregistreaza-rpc.mjs: scoate de pe lant, O DATA, raspunsurile RPC de care are nevoie verifica-plati.mjs pentru un dosar-dovada +// (eth_chainId, chitanta fiecarei plati, Transfer-urile din portofel), ca proba verificatorului sa ruleze apoi fara retea pe date REALE. +// node inregistreaza-rpc.mjs --rpc --out +import fs from 'node:fs'; +import { incarcaEthers } from './wallet.mjs'; + +const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; }; +const f = a[0]; const rpc = get('--rpc'); const out = get('--out'); +if (!f || !rpc || !out) { console.error('usage: node inregistreaza-rpc.mjs --rpc --out '); process.exit(2); } +const ethers = incarcaEthers(); +const d = JSON.parse(fs.readFileSync(f, 'utf8')); +const apel = async (method, params) => { const r = await fetch(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result; }; +const chainId = await apel('eth_chainId', []); +const receipts = {}; +for (const p of d.payments) receipts[p.transaction.toLowerCase()] = await apel('eth_getTransactionReceipt', [p.transaction]); +const logs = await apel('eth_getLogs', [{ address: d.token, fromBlock: '0x' + Number(d.fromBlock).toString(16), toBlock: 'latest', + topics: [ethers.id('Transfer(address,address,uint256)'), '0x' + '0'.repeat(24) + d.wallet.toLowerCase().slice(2)] }]); +fs.writeFileSync(out, JSON.stringify({ v: 1, recordedAt: new Date().toISOString(), rpc, chainId, receipts, transfersOut: logs }, null, 1) + '\n'); +console.log(`recorded chain ${Number(chainId)}: ${Object.keys(receipts).length} receipts, ${logs.length} transfers out of the wallet -> ${out}`); diff --git a/agents/x402/package.json b/agents/x402/package.json new file mode 100644 index 0000000..e25b45a --- /dev/null +++ b/agents/x402/package.json @@ -0,0 +1,9 @@ +{ + "name": "aere-agent-wallet", + "private": true, + "description": "Aere agent wallet: pays x402 only what the agent ledger and the owner policy allow", + "license": "MIT", + "type": "module", + "engines": { "node": ">=24" }, + "dependencies": { "ethers": "6.16.0" } +} diff --git a/agents/x402/proba-verifica-plati.mjs b/agents/x402/proba-verifica-plati.mjs new file mode 100644 index 0000000..7efb312 --- /dev/null +++ b/agents/x402/proba-verifica-plati.mjs @@ -0,0 +1,63 @@ +// Proba verificatorului de plati (verifica-plati.mjs), fara retea: un RPC local care serveste raspunsurile INREGISTRATE de pe testnetul +// 28001 pentru dosarul-dovada din dovezi-28001/ (inregistreaza-rpc.mjs), deci date reale de pe lant. Fiecare verificare are cazul ei +// care trebuie sa o inroseasca, construit din datele reale schimbate intr-un singur loc. +// node proba-verifica-plati.mjs iesire 0 = toate cum trebuia +import fs from 'node:fs'; +import path from 'node:path'; +import http from 'node:http'; +import { fileURLToPath } from 'node:url'; +import { verificaPlati } from './verifica-plati.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const D = path.join(AICI, 'dovezi-28001'); +const dovadaF = fs.readdirSync(D).filter((n) => n.startsWith('plati-agent-') && n.endsWith('.json')).sort().pop(); +const inregF = fs.readdirSync(D).filter((n) => n.startsWith('rpc-inregistrat-') && n.endsWith('.json')).sort().pop(); +let ok = 0, rau = 0; +const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; }; +if (!dovadaF || !inregF) { console.log(`NEMASURAT: lipseste dosarul-dovada sau inregistrarea RPC in ${D}`); process.exit(2); } +const dovada = JSON.parse(fs.readFileSync(path.join(D, dovadaF), 'utf8')); +const inreg = JSON.parse(fs.readFileSync(path.join(D, inregF), 'utf8')); +const copie = (o) => JSON.parse(JSON.stringify(o)); + +// un RPC local peste inregistrare (sau peste o varianta schimbata a ei) +async function rpcDin(rec) { + const srv = http.createServer((req, res) => { let s = ''; req.on('data', (x) => { s += x; }); req.on('end', () => { + const q = JSON.parse(s); let result = null; + if (q.method === 'eth_chainId') result = rec.chainId; + else if (q.method === 'eth_getTransactionReceipt') result = rec.receipts[String(q.params[0]).toLowerCase()] || null; + else if (q.method === 'eth_getLogs') result = rec.transfersOut; + res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: q.id, result })); + }); }); + await new Promise((r) => srv.listen(0, '127.0.0.1', r)); + return { url: `http://127.0.0.1:${srv.address().port}`, close: () => srv.close() }; +} +async function judeca(dov, rec, allTransfers = true) { const r = await rpcDin(rec); try { return await verificaPlati(dov, { rpc: r.url, allTransfers }); } finally { r.close(); } } +const pica = (v, re) => v.checks.some((c) => c.pass === false && re.test(c.name)); + +try { + const v0 = await judeca(dovada, inreg); + cer(v0.verdict === 'VALID' && v0.checks.every((c) => c.pass === true), `1. dosarul real, peste raspunsurile reale de pe 28001: VALID (${v0.checks.length} verificari)`); + const tx1 = dovada.payments[1].transaction.toLowerCase(); + + const d2 = copie(dovada); d2.ledger.entries[dovada.payments[1].entrySeq].body.action.amount = '1'; + cer(pica(await judeca(d2, inreg), /the ledger verifies/), '2. CONTROL: o suma schimbata in registru -> registrul nu mai verifica'); + const d3 = copie(dovada); d3.payments[1].entryHash = 'ab'.repeat(32); + cer(pica(await judeca(d3, inreg), /the entry is in the ledger/), '3. CONTROL: plata numeste o intrare care nu e in registru -> INVALID'); + const d4 = copie(dovada); d4.wallet = '0x' + '11'.repeat(20); + cer(pica(await judeca(d4, inreg, false), /allowed payment from the wallet/), '4. CONTROL: alt portofel decat cel din intrari -> INVALID'); + const r5 = copie(inreg); r5.receipts[tx1].status = '0x0'; + cer(pica(await judeca(dovada, r5), /status 1/), '5. CONTROL: chitanta cu status 0 -> INVALID'); + const r6 = copie(inreg); for (const l of r6.receipts[tx1].logs) if (l.topics.length === 3 && l.topics[2] && !/^0x0{24}/.test(l.topics[2])) l.topics[2] = '0x' + 'cd'.repeat(32); + cer(pica(await judeca(dovada, r6), /AuthorizationUsed/), '6. CONTROL: nonce-ul de pe lant nu e sha256(hash-ul intrarii) -> INVALID'); + const r7 = copie(inreg); for (const l of r7.receipts[tx1].logs) if (l.data && l.data !== '0x') l.data = '0x' + (1n).toString(16).padStart(64, '0'); + cer(pica(await judeca(dovada, r7), /Transfer\(wallet/), '7. CONTROL: suma din Transfer-ul de pe lant alta decat in intrare -> INVALID'); + const r8 = copie(inreg); r8.transfersOut = [...r8.transfersOut, { ...r8.transfersOut[0], transactionHash: '0x' + 'ee'.repeat(32) }]; + cer(pica(await judeca(dovada, r8), /every Transfer out of the wallet/), '8. CONTROL: un Transfer din portofel fara plata in registru -> INVALID'); + const r9 = copie(inreg); r9.chainId = '0xaf0'; + const v9 = await judeca(dovada, r9); + cer(v9.verdict === 'UNMEASURED', `9. CONTROL: un RPC al altui lant (2800) -> ${v9.verdict}, nu VALID si nu INVALID`); + const v10 = await verificaPlati(dovada, { rpc: 'http://127.0.0.1:9', allTransfers: true }); + cer(v10.verdict === 'UNMEASURED', `10. CONTROL: un RPC care nu raspunde -> ${v10.verdict}`); +} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); } +console.log(`\nverifica-plati: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`); +process.exitCode = rau ? 1 : 0; diff --git a/agents/x402/proba-wallet.mjs b/agents/x402/proba-wallet.mjs new file mode 100644 index 0000000..d3906b5 --- /dev/null +++ b/agents/x402/proba-wallet.mjs @@ -0,0 +1,185 @@ +// Proba portofelului de plati x402 al agentilor (wallet.mjs + client.mjs + resource-server.mjs), fara retea: un facilitator LOCAL cu +// aceleasi verificari ca cel de pe testnet (facilitator-local.mjs), semnaturi EIP-712 reale, chei ML-DSA-65 reale. Adversarul are cheia +// AGENTULUI (isi poate scrie si re-semna registrul oricum), nu cheia portofelului si nici cheile oamenilor. Fiecare afirmatie cu perechea +// ei negativa. +// node proba-wallet.mjs iesire 0 = toate cum trebuia +import crypto from 'node:crypto'; +import { definePolicy } from '../agent-policy.mjs'; +import { newAgentIdentity, openLedger, resumeLedger, verifyEquivocation, canonical } from '../agent-ledger.mjs'; +import { newHumanIdentity, approve, revoke } from '../agent-aprobare.mjs'; +import { createWallet, serve, x402Action, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs'; +import { payWithAgent, walletOverHttp, dinB64json } from './client.mjs'; +import { createResourceServer } from './resource-server.mjs'; +import { createLocalFacilitator } from './facilitator-local.mjs'; + +const ethers = incarcaEthers(); +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 NET = 'eip155:28001'; +const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' }; +const ASSET = assetId(NET, TOKEN.address); +const cerinta = (payTo, amount = '10000') => ({ scheme: 'exact', network: NET, amount, asset: TOKEN.address, payTo, maxTimeoutSeconds: 60, extra: { name: TOKEN.name, version: TOKEN.version } }); +const acum = () => Math.floor(Date.now() / 1000); +// o intrare scrisa si semnata de mana cu cheia agentului (adversarul ocoleste biblioteca registrului) +function intrareFalsa(exp, identity, body) { + const r = JSON.parse(JSON.stringify(exp)); const seq = r.entries.length; const prev = seq ? r.entries[seq - 1].hash : '0'.repeat(64); + const b = { v: 2, agentId: r.agentId, policyHash: r.policyHash, session: r.session, provenance: null, ...body, seq }; + const signature = crypto.sign(null, Buffer.from(`${seq}|${prev}|${canonical(b)}`, 'utf8'), identity.privateKey).toString('base64'); + r.entries.push({ seq, prev, body: b, signature, hash: crypto.createHash('sha256').update(`${seq}|${prev}|${canonical(b)}|${signature}`).digest('hex') }); + return r; +} +// un agent cu portofelul lui: politica numeste portofelul, limita 50000 pe ora in activul x402 +function agentCuPortofel({ limita = '50000', extra = {}, payee, state } = {}) { + const agent = newAgentIdentity(); const cheie = ethers.Wallet.createRandom(); + const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: limita, windowSeconds: 3600, asset: ASSET }, + recipients: payee ? [payee] : undefined, wallet: cheie.address, ...extra }); + const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN, state }); + return { agent, cheie, policy, policyHash, wallet }; +} + +// serverele deschise se inchid in `finally`, si cand proba s-a oprit la jumatate: altfel procesul ramane viu dupa rezumat +// (masurat: controlul negativ astepta 240 s pe o copie oprita la jumatate) +const deschise = []; +const inchide = (s) => { if (!s) return; try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* deja inchis */ } }; +// un pas care arunca (o plantare care strica un raspuns) inroseste proba cu motivul, nu o opreste inainte de rezumat +try { +const payee = ethers.Wallet.createRandom().address.toLowerCase(); +const A = agentCuPortofel({ payee }); +const fac = createLocalFacilitator({ network: NET, token: TOKEN, balances: { [A.cheie.address]: '1000000' } }); +const facUrl = await fac.listen(); deschise.push(fac.server); +const rs = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); +const url = await rs.listen(); deschise.push(rs.server); +const L = openLedger({ identity: A.agent, policy: A.policy, policyHash: A.policyHash }); + +// 1. cinci cumparaturi platite prin facilitator, fiecare scrisa intai in registru +const plati = []; +for (let i = 0; i < 5; i++) plati.push(await payWithAgent({ url, ledger: L, wallet: A.wallet })); +cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && p.settlement.success), `1. cinci cumparaturi de 10000: platite si servite (${plati.map((p) => p.status).join(',')})`); +cer(fac.balanceOf(payee) === 50000n && fac.balanceOf(A.cheie.address) === 950000n, `1. soldurile la facilitator: vanzatorul 50000, portofelul 950000 (${fac.balanceOf(payee)})`); +cer(plati.every((p) => p.receipt.nonce === nonceForEntry(p.entry.hash) && fac.used.has(`${A.cheie.address.toLowerCase()}:${p.receipt.nonce}`)), '1. nonce-ul fiecarei autorizari EIP-3009 e sha256(hash-ul intrarii din registru), si chiar el e consumat'); +cer(plati.every((p) => p.decision.statement.allowed === true && p.decision.statement.policyHash === A.policyHash), '1. fiecare plata vine cu plicul AIP-23 al deciziei, sub politica fixata'); + +// 2. a sasea depaseste limita: politica agentului o refuza, portofelul nici nu e chemat +const p6 = await payWithAgent({ url, ledger: L, wallet: A.wallet }); +cer(!p6.paid && /policy refused.*over the limit/.test(p6.reason) && fac.balanceOf(payee) === 50000n && A.wallet.status().signed === 5, `2. CONTROL: a sasea cumparatura depaseste limita: refuzata inainte de portofel, soldul neschimbat (${p6.reason})`); + +// 3. ATAC: agentul ocoleste biblioteca si scrie singur o plata "permisa" peste limita, re-semnata cu cheia lui +const r0 = await fetch(url); const pr = dinB64json(r0.headers.get('payment-required')); +const falsa = intrareFalsa(L.export(), A.agent, { at: acum(), action: { ...x402Action(A.cheie.address, pr.resource, pr.accepts[0]), at: acum() }, decision: { allowed: true, reason: 'under the limit' } }); +const r3 = await A.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: falsa }); +cer(!r3.ok && /false decision/.test(r3.error) && A.wallet.status().signed === 5, `3. ATAC: plata "permisa" peste limita, scrisa si semnata de agent -> portofelul re-ruleaza politica si refuza (${(r3.error || "").slice(0, 90)})`); + +// 4. ATAC: intrarea numeste alta cumparatura decat cea platita (alta suma / alt destinatar) +const B = agentCuPortofel({ payee }); +const LB = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); +LB.record({ ...x402Action(B.cheie.address, pr.resource, pr.accepts[0]), amount: '100' }); +const r4 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB.export() }); +cer(!r4.ok && /another amount/.test(r4.error), `4. ATAC: intrarea spune 100, cumparatura costa 10000 -> refuzat (${r4.error})`); +const LB2 = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); // o intrare curata, pentru controlul pozitiv +LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0])); +const r4b = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); +cer(r4b.ok, '4. CONTROL: aceeasi cumparatura, cu intrarea care o numeste exact -> semnata'); + +// 5. ATAC: acelasi registru trimis din nou (aceeasi ultima intrare) +const r5 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); +cer(!r5.ok && /already seen/.test(r5.error), `5. ATAC: aceeasi intrare a doua oara -> refuzat (${r5.error})`); + +// 6. ATAC: o ramura (registrul luat de la un prefix si continuat altfel) -> refuzat, cu dovada de echivocare +LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0])); +const r6a = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); +const prefix = { ...LB2.export(), entries: LB2.export().entries.slice(0, 1) }; +const ramura = resumeLedger({ identity: B.agent, policy: B.policy, ledger: prefix }); +ramura.record(x402Action(B.cheie.address, { url: 'http://altundeva/x' }, pr.accepts[0])); +const r6 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: { url: 'http://altundeva/x' }, ledger: ramura.export() }); +cer(r6a.ok && !r6.ok && /another branch/.test(r6.error), `6. ATAC: o ramura a registrului -> refuzata (${(r6.error || "").slice(0, 80)})`); +cer(!!r6.equivocation && verifyEquivocation(r6.equivocation).ok, '6. si portofelul da dovada de echivocare, verificabila de oricine cu cheia publica a agentului'); + +// 7. politica schimbata: registrul nou incepe de la zero, portofelul isi aminteste ce a semnat +const C = agentCuPortofel({ payee }); +const LC = openLedger({ identity: C.agent, policy: C.policy, policyHash: C.policyHash }); +let semnate = 0; +for (let i = 0; i < 5; i++) { LC.record(x402Action(C.cheie.address, { url: 'http://r/' + i }, cerinta(payee))); if ((await C.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/' + i }, ledger: LC.export() })).ok) semnate++; } +const polB = definePolicy({ ...C.policy, tools: ['search'] }); +const Cb = createWallet({ policy: polB.policy, policyHash: polB.policyHash, evmPrivateKey: C.cheie.privateKey, network: NET, token: TOKEN, state: C.wallet.exportState() }); +const LCb = openLedger({ identity: C.agent, policy: polB.policy, policyHash: polB.policyHash }); +const rc = LCb.record(x402Action(C.cheie.address, { url: 'http://r/nou' }, cerinta(payee))); +const r7 = await Cb.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/nou' }, ledger: LCb.export() }); +cer(semnate === 5 && rc.allowed && !r7.ok && /by what this wallet has signed.*over the limit/.test(r7.error), `7. politica noua, registru nou (el permite: ${rc.allowed}), dar portofelul a semnat deja 50000 in ora asta -> refuzat (${(r7.error || '').slice(0, 70)})`); + +// 8. revocarea data portofelului de proprietar, pe care agentul nu o scrie in registru +const O = newHumanIdentity(); +const D = agentCuPortofel({ payee, extra: { owner: O.humanId } }); +const LD = openLedger({ identity: D.agent, policy: D.policy, policyHash: D.policyHash }); +LD.record(x402Action(D.cheie.address, { url: 'http://d/1' }, cerinta(payee))); +const r8a = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/1' }, ledger: LD.export() }); +cer(!D.wallet.addRevocation(revoke({ owner: newHumanIdentity(), agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. CONTROL: o revocare semnata de altcineva decat proprietarul e refuzata de portofel'); +cer(D.wallet.addRevocation(revoke({ owner: O, agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. revocarea proprietarului e primita de portofel'); +LD.record(x402Action(D.cheie.address, { url: 'http://d/2' }, cerinta(payee))); +const r8 = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/2' }, ledger: LD.export() }); +cer(r8a.ok && !r8.ok && /false decision.*revoked/.test(r8.error), `8. dupa revocare, plata urmatoare e refuzata desi agentul nu a scris revocarea (${(r8.error || '').slice(0, 80)})`); + +// 9. aprobarea umana: peste 20000 cere un om +const H = newHumanIdentity(); +const E = agentCuPortofel({ payee, limita: '100000', extra: { approval: { approvers: [H.humanId], threshold: 1, above: '20000' } } }); +const LE = openLedger({ identity: E.agent, policy: E.policy, policyHash: E.policyHash }); +const mare = cerinta(payee, '30000'); +const fara = LE.record(x402Action(E.cheie.address, { url: 'http://e/1' }, mare)); +const act = x402Action(E.cheie.address, { url: 'http://e/2' }, mare); +const apr = approve({ human: H, agentId: E.agent.agentId, policyHash: E.policyHash, action: act, issuedAt: acum() - 5, expiresAt: acum() + 300 }); +LE.record(act, { approvals: [apr] }); +const r9 = await E.wallet.authorize({ requirements: mare, resource: { url: 'http://e/2' }, ledger: LE.export() }); +cer(!fara.allowed && r9.ok, `9. 30000 fara aprobare: refuzat de politica; cu aprobarea omului numit: semnat (${fara.reason})`); + +// 10. ATAC: o plata antedatata (ceasul agentului dat inapoi) +const F = agentCuPortofel({ payee }); +const LF = openLedger({ identity: F.agent, policy: F.policy, policyHash: F.policyHash, now: () => acum() - 600 }); +LF.record(x402Action(F.cheie.address, { url: 'http://f/1' }, cerinta(payee))); +const r10 = await F.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://f/1' }, ledger: LF.export() }); +cer(!r10.ok && /not now/.test(r10.error), `10. ATAC: plata datata cu 10 minute in urma -> refuzata (${(r10.error || '').slice(0, 70)})`); + +// 11. serverul de resurse: plata pentru alta cerinta, si reluarea aceleiasi plati +const alta = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify({ ...r4b.paymentPayload, accepted: cerinta(payee, '1') })).toString('base64') } }); +cer(alta.status === 402 && /another requirement/.test(await alta.text()), '11. CONTROL: o plata pentru alta cerinta decat a emis serverul -> 402'); +fac.fund(B.cheie.address, '100000'); // portofelul lui B are acum sold la facilitator +const reluare = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } }); +const reluare2 = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } }); +cer(reluare.status === 200 && reluare2.status === 402 && /nonce_already_used/.test(await reluare2.text()), '11. aceeasi plata de doua ori: prima data servita, a doua oara 402 (nonce folosit la facilitator)'); + +// 12. doua ramuri la aceeasi pozitie, trimise DEODATA: portofelul semneaza cel mult una +const G = agentCuPortofel({ payee }); +const LG1 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash }); +const LG2 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash }); +LG1.record(x402Action(G.cheie.address, { url: 'http://g/1' }, cerinta(payee, '50000'))); +LG2.record(x402Action(G.cheie.address, { url: 'http://g/2' }, cerinta(payee, '50000'))); +const [g1, g2] = await Promise.all([G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/1' }, ledger: LG1.export() }), + G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/2' }, ledger: LG2.export() })]); +cer([g1, g2].filter((x) => x.ok).length === 1 && G.wallet.status().signed === 1, `12. ATAC: doua ramuri de cate 50000 trimise deodata (limita 50000) -> exact una semnata (${[g1.ok, g2.ok]})`); + +// 13. portofelul prin HTTP, cap la cap +const srv = await serve(A.wallet, { port: 0 }); deschise.push(srv); +const W = walletOverHttp(`http://127.0.0.1:${srv.address().port}`); +const st = await W.status(); +const LA = resumeLedger({ identity: A.agent, policy: A.policy, ledger: L.export() }); +const p13 = await payWithAgent({ url, ledger: LA, wallet: W }); +cer(st.address === A.cheie.address && st.policyHash === A.policyHash && !p13.paid && /over the limit/.test(p13.reason), `13. prin HTTP: /status spune portofelul si politica; limita tine si dupa reluarea registrului (${(p13.reason || '').slice(0, 60)})`); +const H2 = agentCuPortofel({ payee }); const srv2 = await serve(H2.wallet, { port: 0 }); deschise.push(srv2); +const rs2 = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url2 = await rs2.listen(); deschise.push(rs2.server); +const LH = openLedger({ identity: H2.agent, policy: H2.policy, policyHash: H2.policyHash }); +const p13b = await payWithAgent({ url: url2, ledger: LH, wallet: walletOverHttp(`http://127.0.0.1:${srv2.address().port}`) }); +cer(p13b.status === 402 && /wallet refused|insufficient_funds|not valid/.test(p13b.reason || ''), `13. CONTROL: un portofel fara sold -> plata nu trece la facilitator (${(p13b.reason || '').slice(0, 70)})`); + +// 14. portofelul refuza sa porneasca pe o politica ce nu il numeste +const alt = ethers.Wallet.createRandom(); +cer(/does not name this wallet/.test(arunca(() => createWallet({ policy: A.policy, evmPrivateKey: alt.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: o politica ce numeste alt portofel -> portofelul nu porneste'); +cer(/pinned policyHash/.test(arunca(() => createWallet({ policy: A.policy, policyHash: '0x' + '00'.repeat(32), evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: un hash fixat care nu e al politicii -> portofelul nu porneste'); +const faraLimita = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address }).policy; +const altActiv = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address, spend: { amount: '1', windowSeconds: 60 } }).policy; +cer(/spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: faraLimita, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || '') + && /spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: altActiv, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), + '14. CONTROL: o politica fara limita, sau cu limita in alt activ (AERE) -> portofelul nu porneste'); + +} catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); } finally { for (const s of deschise) inchide(s); } + +console.log(`\nagent-wallet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`); +process.exitCode = rau ? 1 : 0; diff --git a/agents/x402/proba-x402-testnet.mjs b/agents/x402/proba-x402-testnet.mjs new file mode 100644 index 0000000..9221ef3 --- /dev/null +++ b/agents/x402/proba-x402-testnet.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +// Proba cap la cap pe testnetul PUBLIC 28001 (2026-09-29, punctele 23, 25): un agent AI cumpara o resursa x402 de pe un server de +// resurse local, platind prin portofelul lui (wallet.mjs) si prin facilitatorul x402 de pe testnet, cu decontare EIP-3009 pe lant. +// 1. un portofel NOU (cheie generata aici, tinuta numai in memorie) primeste 0,05 tUSD de la cheia de dezvoltator a testnetului +// (singura tranzactie trimisa de proba; decontarile le plateste facilitatorul); +// 2. politica agentului: 30000 (0,03 tUSD) pe ora, un singur destinatar, portofelul numit; +// 3. trei cumparaturi de 10000 platite si servite; a patra refuzata de politica, inainte de portofel; aceeasi plata trimisa din nou: +// 402 (nonce folosit pe lant); +// 4. pe lant: soldurile, si authorizationState(portofel, sha256(hash-ul intrarii)) pentru fiecare plata; +// 5. dosarul-dovada scris (registrul, politica, platile) si verificat cu verifica-plati.mjs: VALID, inclusiv "orice Transfer din +// portofel e o plata din registru"; o copie cu o suma schimbata in registru: INVALID. +// Refuza orice alt lant decat 28001 (citeste eth_chainId). Cheia de dezvoltator se citeste din fisierul dat in AERE_TESTNET_KEY_FILE +// (un rand d= sau PRIVATE_KEY=0x) si nu se tipareste. +// AERE_TESTNET_KEY_FILE= node proba-x402-testnet.mjs [--rpc URL] [--facilitator URL] +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { definePolicy } from '../agent-policy.mjs'; +import { newAgentIdentity, openLedger } from '../agent-ledger.mjs'; +import { createWallet, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs'; +import { payWithAgent } from './client.mjs'; +import { createResourceServer } from './resource-server.mjs'; +import { verificaPlati } from './verifica-plati.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const a = process.argv.slice(2); const get = (f, d) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : d; }; +const RPC = get('--rpc', 'https://testnet-rpc.aere.network'); +const FAC = get('--facilitator', 'https://testnet-rpc.aere.network/x402'); +const NET = 'eip155:28001'; +const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' }; +const ethers = incarcaEthers(); +let ok = 0, rau = 0; +const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; return c; }; +const taie = (s) => String(s || '').replace(/(0x)?[0-9a-fA-F]{60,}/g, '').slice(0, 160); + +const kf = process.env.AERE_TESTNET_KEY_FILE; +if (!kf || !fs.existsSync(kf)) { console.log('NEMASURAT: AERE_TESTNET_KEY_FILE nu numeste un fisier cu cheia de dezvoltator a testnetului'); process.exit(2); } +const rand = fs.readFileSync(kf, 'utf8').split(/\r?\n/).map((l) => l.trim()).find((l) => /^(d|PRIVATE_KEY)=/.test(l)) || ''; +const hex = rand.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim(); +if (!/^[0-9a-fA-F]{1,64}$/.test(hex)) { console.log('NEMASURAT: fisierul cheii nu are un rand d= sau PRIVATE_KEY=0x'); process.exit(2); } +const provider = new ethers.JsonRpcProvider(RPC, undefined, { staticNetwork: false }); +const lant = Number((await provider.getNetwork()).chainId); +if (lant !== 28001) { console.log(`REFUZ: RPC-ul serveste lantul ${lant}; proba ruleaza numai pe testnetul 28001`); process.exit(2); } +const dev = new ethers.Wallet('0x' + hex.padStart(64, '0'), provider); +const tUSD = new ethers.Contract(TOKEN.address, ['function transfer(address,uint256) returns (bool)', 'function balanceOf(address) view returns (uint256)', + 'function authorizationState(address,bytes32) view returns (bool)'], dev); + +try { + // 1. portofelul nou, finantat + const cheie = ethers.Wallet.createRandom(); const payee = ethers.Wallet.createRandom().address; + const tx0 = await tUSD.transfer(cheie.address, 50000n); const rc0 = await tx0.wait(1, 120000); + cer(rc0 && rc0.status === 1, `1. portofelul nou ${cheie.address} primeste 0,05 tUSD (bloc ${rc0 && rc0.blockNumber})`); + // 2. agentul si politica lui + const agent = newAgentIdentity(); + const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: '30000', windowSeconds: 3600, asset: assetId(NET, TOKEN.address) }, + recipients: [payee], wallet: cheie.address }); + const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN }); + const L = openLedger({ identity: agent, policy, policyHash }); + const cerinta = { scheme: 'exact', network: NET, amount: '10000', asset: TOKEN.address, payTo: payee, maxTimeoutSeconds: 120, extra: { name: TOKEN.name, version: TOKEN.version } }; + const rs = createResourceServer({ requirement: cerinta, facilitator: FAC, content: 'the paid content' }); + const url = await rs.listen(); + // 3. trei cumparaturi platite, a patra refuzata + const plati = []; + for (let i = 0; i < 3; i++) plati.push(await payWithAgent({ url, ledger: L, wallet })); + cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && /^0x[0-9a-f]{64}$/.test(p.settlement.transaction || '')), + `3. trei cumparaturi de 0,01 tUSD platite prin facilitatorul testnetului si servite (${plati.map((p) => p.status + (p.reason ? ' ' + taie(p.reason) : '')).join('; ')})`); + const p4 = await payWithAgent({ url, ledger: L, wallet }); + cer(!p4.paid && /policy refused.*over the limit/.test(p4.reason || '') && wallet.status().signed === 3, `3. CONTROL: a patra depaseste 0,03 pe ora: refuzata de politica, portofelul a semnat tot 3 (${taie(p4.reason)})`); + // reluarea: acelasi PaymentPayload pe care serverul l-a primit la prima plata + const prima = rs.lastPayloads ? rs.lastPayloads[0] : null; + if (prima) { + const r = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(prima)).toString('base64') } }); + cer(r.status === 402 && /nonce_already_used|already/.test(await r.text()), '3. CONTROL: prima plata trimisa din nou -> 402 (autorizarea e deja folosita pe lant)'); + } else cer(false, '3. serverul de resurse nu a pastrat plata primita (lastPayloads)'); + rs.server.close(); + // 4. pe lant + await new Promise((r) => setTimeout(r, 2000)); + const soldPayee = await tUSD.balanceOf(payee), soldPortofel = await tUSD.balanceOf(cheie.address); + cer(soldPayee === 30000n && soldPortofel === 20000n, `4. pe lant: vanzatorul are 30000, portofelul 20000 (${soldPayee}, ${soldPortofel})`); + const folosite = await Promise.all(plati.map((p) => tUSD.authorizationState(cheie.address, nonceForEntry(p.entry.hash)))); + cer(folosite.every(Boolean), '4. pe lant: autorizarea cu nonce-ul sha256(hash-ul intrarii) e folosita, pentru fiecare plata'); + // 5. dosarul-dovada, verificat din afara + const dovada = { v: 1, kind: 'aere-agent-x402-payments', network: NET, token: TOKEN.address, wallet: cheie.address, fromBlock: rc0.blockNumber, + facilitator: FAC, policy, policyHash, ledger: L.export(), + payments: plati.map((p) => ({ entrySeq: p.entry.seq, entryHash: p.entry.hash, transaction: p.settlement.transaction })), createdAt: new Date().toISOString() }; + const dir = path.join(AICI, 'dovezi-28001'); fs.mkdirSync(dir, { recursive: true }); + const f = path.join(dir, `plati-agent-${dovada.createdAt.slice(0, 19).replace(/[:T]/g, '-')}.json`); + fs.writeFileSync(f, JSON.stringify(dovada, null, 1) + '\n'); + const v = await verificaPlati(dovada, { rpc: RPC, allTransfers: true }); + cer(v.verdict === 'VALID', `5. verifica-plati pe dosarul-dovada: ${v.verdict} (${v.checks.length} verificari, printre ele: orice Transfer din portofel e o plata din registru)`); + const stricat = JSON.parse(JSON.stringify(dovada)); stricat.ledger.entries[plati[1].entry.seq].body.action.amount = '1'; + const vs = await verificaPlati(stricat, { rpc: RPC }); + cer(vs.verdict === 'INVALID', `5. CONTROL: o suma schimbata in registrul din dosar -> ${vs.verdict}`); + const fara = JSON.parse(JSON.stringify(dovada)); fara.payments = fara.payments.slice(1); + const vf = await verificaPlati(fara, { rpc: RPC, allTransfers: true }); + cer(vf.verdict === 'INVALID', `5. CONTROL: o plata de pe lant scoasa din dosar -> ${vf.verdict} (un Transfer din portofel fara intrare)`); + console.log(` dosarul-dovada: ${path.relative(process.cwd(), f)}`); +} catch (e) { cer(false, `proba s-a oprit: ${taie(e.shortMessage || e.message)}`); } +console.log(`\nagent-x402-testnet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`); +process.exitCode = rau ? 1 : 0; diff --git a/agents/x402/resource-server.mjs b/agents/x402/resource-server.mjs new file mode 100644 index 0000000..1813dec --- /dev/null +++ b/agents/x402/resource-server.mjs @@ -0,0 +1,39 @@ +// Un server de resurse x402 v2, minimal (2026-09-29): raspunde 402 cu PAYMENT-REQUIRED, iar la reluare trimite plata facilitatorului +// (POST /verify, apoi POST /settle) si serveste resursa numai dupa o decontare reusita, cu PAYMENT-RESPONSE. E piesa pe care o are +// orice vanzator x402; aici e ca probele agentilor sa plateasca cap la cap printr-un facilitator adevarat (cel de pe testnetul 28001) +// sau printr-unul local. Nu are voie sa accepte o plata pentru alta cerinta decat a emis-o: `accepted` trebuie sa fie chiar ea. +import http from 'node:http'; +import { b64json, dinB64json } from './client.mjs'; + +/** + * @param {object} o { requirement:{scheme,network,amount,asset,payTo,maxTimeoutSeconds,extra}, facilitator: URL, path, content, description } + */ +export function createResourceServer({ requirement, facilitator, path: cale = '/premium', content = 'the paid content', description = 'a paid resource', fetchImpl = fetch }) { + const fac = String(facilitator).replace(/\/+$/, ''); + const post = async (p, body) => { const r = await fetchImpl(fac + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); return r.json(); }; + const cheie = (x) => JSON.stringify(['scheme', 'network', 'amount', 'asset', 'payTo', 'maxTimeoutSeconds'].map((k) => String(x && x[k]).toLowerCase())); + const decontari = [], primite = []; + const srv = http.createServer(async (req, res) => { + const url = (req.url || '/').split('?')[0]; + if (url !== cale) { res.writeHead(404); return res.end('not found'); } + const resource = { url: `http://${req.headers.host}${cale}`, description, mimeType: 'text/plain' }; + const cere = (error) => { const pr = { x402Version: 2, error, resource, accepts: [requirement] }; + res.writeHead(402, { 'content-type': 'application/json', 'PAYMENT-REQUIRED': b64json(pr) }); res.end(JSON.stringify(pr)); }; + const h = req.headers['payment-signature']; + if (!h) return cere('payment required'); + const payload = dinB64json(h); + if (!payload || payload.x402Version !== 2 || !payload.payload) return cere('the PAYMENT-SIGNATURE header is not an x402 v2 payment payload'); + if (cheie(payload.accepted) !== cheie(requirement)) return cere('the payment is for another requirement than this resource issued'); + primite.push(payload); + try { + const body = { x402Version: 2, paymentPayload: payload, paymentRequirements: requirement }; + const v = await post('/verify', body); + if (!v.isValid) return cere(`payment not valid: ${v.invalidReason}`); + const s = await post('/settle', body); + decontari.push(s); + if (!s.success) return cere(`settlement failed: ${String(s.errorReason).slice(0, 120)}`); + res.writeHead(200, { 'content-type': 'text/plain', 'PAYMENT-RESPONSE': b64json(s) }); res.end(content); + } catch (e) { res.writeHead(502, { 'content-type': 'text/plain' }); res.end('facilitator unreachable: ' + String(e.message).slice(0, 80)); } + }); + return { server: srv, settlements: decontari, lastPayloads: primite, listen: (port = 0, host = '127.0.0.1') => new Promise((r) => srv.listen(port, host, () => r(`http://${host}:${srv.address().port}${cale}`))) }; +} diff --git a/agents/x402/verifica-plati.mjs b/agents/x402/verifica-plati.mjs new file mode 100644 index 0000000..2d554f5 --- /dev/null +++ b/agents/x402/verifica-plati.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// verifica-plati.mjs: dovada ca platile de pe lant ale unui portofel de agent au fost PERMISE de politica agentului, verificata de +// oricine, din afara (2026-09-29, punctele 23, 25). Primeste o dosar-dovada {policy, policyHash, ledger, wallet, network, token, +// payments:[{entrySeq, entryHash, transaction}], fromBlock?} si un RPC, si cere: +// 1. registrul verifica fara incredere sub politica (verifyLedger); +// 2. fiecare plata numeste o intrare a registrului: plata permisa, din portofel, catre destinatar, cu suma si activul; +// 3. pe lant, tranzactia platii are status 1 si emite, din contractul activului, AuthorizationUsed(portofel, nonce) cu nonce = +// sha256(hash-ul intrarii) si Transfer(portofel, destinatar, suma) - deci plata de pe lant e chiar cea din registru; +// 4. cu --all-transfers: ORICE Transfer din portofel in intervalul de blocuri e una din platile de mai sus (nicio plata fara intrare). +// Ce nu dovedeste: ca serviciul cumparat a fost livrat; ca portofelul nu a semnat si autorizari nedecontate (acelea nu misca bani). +// node verifica-plati.mjs --rpc [--all-transfers] iesire 0 VALID, 1 INVALID, 2 NEMASURAT +import fs from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { verifyLedger } from '../agent-ledger.mjs'; +import { incarcaEthers, nonceForEntry, assetId } from './wallet.mjs'; + +export async function verificaPlati(d, { rpc, allTransfers = false, fetchImpl = fetch } = {}) { + const ethers = incarcaEthers(); + const rez = []; const ok = (name, pass, detail = '') => { rez.push({ name, pass, detail }); return pass; }; + const apel = async (method, params) => { + const r = await fetchImpl(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); + const j = await r.json(); if (j.error) throw new Error(`${method}: ${String(j.error.message).slice(0, 80)}`); return j.result; + }; + const chain = Number(String(d.network).split(':')[1]); + let lantul; + try { lantul = Number(await apel('eth_chainId', [])); } catch (e) { return { verdict: 'UNMEASURED', checks: [{ name: 'rpc', pass: null, detail: e.message }] }; } + if (!ok(`the RPC serves chain ${chain}`, lantul === chain, `it serves ${lantul}`)) return { verdict: 'UNMEASURED', checks: rez }; + const v = verifyLedger(d.ledger, { policy: d.policy, policyHash: d.policyHash, revocations: d.revocations || [] }); + ok(`the ledger verifies under the policy (${d.ledger.entries.length} entries)`, v.ok, v.error || ''); + const token = String(d.token).toLowerCase(), wallet = String(d.wallet).toLowerCase(), asset = assetId(d.network, token); + const T_AUTH = ethers.id('AuthorizationUsed(address,bytes32)'), T_TRANSFER = ethers.id('Transfer(address,address,uint256)'); + const topicAdresa = (a) => '0x' + '0'.repeat(24) + String(a).toLowerCase().slice(2); + const txPlati = new Set(); + for (const p of d.payments || []) { + const e = d.ledger.entries[p.entrySeq]; + if (!ok(`payment ${p.entrySeq}: the entry is in the ledger`, !!e && e.hash === p.entryHash)) continue; + const a = e.body.action; + ok(`payment ${p.entrySeq}: the entry is an allowed payment from the wallet in the asset`, e.body.decision.allowed && a.kind === 'payment' && String(a.from).toLowerCase() === wallet && a.asset === asset); + let rc; + try { rc = await apel('eth_getTransactionReceipt', [p.transaction]); } catch (x) { ok(`payment ${p.entrySeq}: receipt readable`, false, x.message); continue; } + if (!ok(`payment ${p.entrySeq}: transaction ${String(p.transaction).slice(0, 12)} is on chain with status 1`, !!rc && rc.status === '0x1')) continue; + txPlati.add(String(p.transaction).toLowerCase()); + const logs = rc.logs.filter((l) => l.address.toLowerCase() === token); + const nonce = nonceForEntry(e.hash).toLowerCase(); + ok(`payment ${p.entrySeq}: AuthorizationUsed(wallet, sha256(entry hash)) is emitted by the asset`, logs.some((l) => l.topics[0] === T_AUTH && l.topics[1].toLowerCase() === topicAdresa(wallet) && l.topics[2].toLowerCase() === nonce)); + ok(`payment ${p.entrySeq}: Transfer(wallet, ${String(a.to).slice(0, 10)}.., ${a.amount}) is emitted by the asset`, logs.some((l) => l.topics[0] === T_TRANSFER && l.topics[1].toLowerCase() === topicAdresa(wallet) + && l.topics[2].toLowerCase() === topicAdresa(a.to) && BigInt(l.data) === BigInt(a.amount))); + } + if (allTransfers) { + try { + const logs = await apel('eth_getLogs', [{ address: token, fromBlock: '0x' + Number(d.fromBlock || 0).toString(16), toBlock: 'latest', topics: [T_TRANSFER, topicAdresa(wallet)] }]); + const straine = logs.filter((l) => !txPlati.has(l.transactionHash.toLowerCase())); + ok(`every Transfer out of the wallet since block ${d.fromBlock || 0} is a payment in the ledger (${logs.length} transfers)`, straine.length === 0, straine.map((l) => l.transactionHash.slice(0, 12)).join(', ')); + } catch (x) { rez.push({ name: 'all transfers out of the wallet', pass: null, detail: x.message }); } + } + const verdict = rez.some((c) => c.pass === false) ? 'INVALID' : rez.some((c) => c.pass === null) ? 'UNMEASURED' : 'VALID'; + return { verdict, checks: rez }; +} + +if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) { + const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; }; + const f = a.find((x) => !x.startsWith('--') && x !== get('--rpc')); + if (!f || !get('--rpc')) { console.error('usage: node verifica-plati.mjs --rpc [--all-transfers]'); process.exitCode = 2; } + else { + const r = await verificaPlati(JSON.parse(fs.readFileSync(f, 'utf8')), { rpc: get('--rpc'), allTransfers: a.includes('--all-transfers') }); + for (const c of r.checks) console.log(` ${c.pass === true ? 'OK ' : c.pass === false ? 'FAIL' : '-- '} ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`); + console.log(r.verdict); + process.exitCode = r.verdict === 'VALID' ? 0 : r.verdict === 'INVALID' ? 1 : 2; + } +} diff --git a/agents/x402/wallet.mjs b/agents/x402/wallet.mjs new file mode 100644 index 0000000..6422084 --- /dev/null +++ b/agents/x402/wallet.mjs @@ -0,0 +1,218 @@ +#!/usr/bin/env node +// Aere Agent Wallet (roadmap punctele 23, 25, 26, 2026-09-29): portofelul de plati x402 al unui agent AI, care semneaza o plata NUMAI +// daca agentul a inregistrat-o in registrul lui (agent-ledger.mjs) si politica proprietarului o permite. Agentul NU tine cheia de +// plata: o tine portofelul (serviciul proprietarului). Agentul are numai cheia lui ML-DSA-65 cu care isi semneaza registrul; cererea +// catre portofel e autentificata chiar de intrarea noua din registru, pe care numai agentul o poate semna. +// +// CE VERIFICA, la fiecare cerere (authorize): +// 1. cerinta x402 (schema exact, reteaua si activul portofelului, payTo, suma intreaga, termenul); +// 2. registrul intreg, fara incredere (verifyLedger): politica FIXATA de proprietar la pornire (hash-ul ei), identitatea, fiecare +// semnatura, lantul, decizia re-rulata, revocarile primite de portofel de la proprietar, si ANCORELE: capetele pe care portofelul +// le-a vazut el insusi, cu ora lui. Portofelul e martorul: un registru care nu continua ce a vazut e o ramura (si daca a pastrat +// intrarea, scoate dovada de echivocare), iar o intrare scrisa dupa un cap nu poate declara o ora dinaintea lui; +// 3. ultima intrare e o plata NOUA, permisa, din portofelul acesta, catre payTo, cu suma si activul cerute, legata de cumparatura prin +// `ref` (hash-ul cerintei), scrisa ACUM dupa ceasul portofelului; +// 4. limita, a doua oara, fata de ce a SEMNAT portofelul (nu numai fata de ce spune registrul): chiar un registru rescris sau ramificat +// nu poate scoate din portofel mai mult decat permite politica. +// Apoi semneaza o autorizare EIP-3009 (TransferWithAuthorization) al carei nonce e sha256(hash-ul intrarii): plata de pe lant se leaga +// de intrarea din registru, si aceeasi intrare nu poate plati de doua ori (nici la portofel, nici pe lant). Intoarce PaymentPayload-ul +// x402 v2, de pus de agent in antetul PAYMENT-SIGNATURE, si un plic AIP-23 cu decizia. +// +// CE NU FACE: nu trimite tranzactii (decontarea o face facilitatorul x402 al serverului de resurse); nu tine alt activ decat unul +// EIP-3009 configurat; nu e custodie fara incredere: cine tine cheia portofelului (proprietarul) poate plati fara agent. Un portofel- +// contract 2-din-2 (agentul + politica, ERC-1271) ar scoate si increderea asta; nu e facut. +// +// node wallet.mjs serve --config wallet.json [--port 8793] serviciul HTTP (POST /authorize, POST /revocations, GET /status) +// wallet.json: { policy, policyHash, network, token:{address,name,version}, evmKeyFile, stateFile, toleranceSeconds?, maxValiditySeconds? } +import fs from 'node:fs'; +import path from 'node:path'; +import http from 'node:http'; +import crypto from 'node:crypto'; +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { hashPolicy, checkAction, decisionEnvelope, canonical } from '../agent-policy.mjs'; +import { verifyLedger, verifyEquivocation, sessionId } from '../agent-ledger.mjs'; +import { verifyApproval, verifyRevocation } from '../agent-aprobare.mjs'; + +export const VERSION = 'aere-agent-wallet/1 (2026-09-29)'; +const AICI = path.dirname(fileURLToPath(import.meta.url)); + +/** ethers: AERE_ETHERS (o cale, pentru copiile controlului negativ), langa portofel (npm install aici), sau din contracts/node_modules */ +export function incarcaEthers() { + const req = createRequire(import.meta.url); + if (process.env.AERE_ETHERS) return req(process.env.AERE_ETHERS); + try { return req('ethers'); } catch { /* nu e langa portofel */ } + try { return req(path.resolve(AICI, '..', '..', '..', 'contracts', 'node_modules', 'ethers')); } catch { /* nici in depozitul de dezvoltare */ } + throw new Error('the wallet needs the ethers package: run `npm install` in this directory'); +} +const sha = (s) => '0x' + crypto.createHash('sha256').update(typeof s === 'string' ? Buffer.from(s, 'utf8') : s).digest('hex'); +const ADRESA = /^0x[0-9a-fA-F]{40}$/; +const INTREG = /^[0-9]+$/; +const TIPURI = { TransferWithAuthorization: [ + { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, + { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }] }; +export { TIPURI as EIP3009_TYPES }; + +/** activul, ca identificator CAIP-19: eip155:/erc20: */ +export const assetId = (network, token) => `${network}/erc20:${String(token).toLowerCase()}`; +/** legatura intrarii din registru cu cumparatura: hash-ul canonic al resursei si al cerintei x402 */ +export function purchaseRef(resource, req) { + return sha(canonical({ resource: resource && resource.url ? String(resource.url) : null, scheme: String(req.scheme), network: String(req.network), + asset: String(req.asset).toLowerCase(), payTo: String(req.payTo).toLowerCase(), amount: String(req.amount) })); +} +/** actiunea pe care agentul o scrie in registru inainte sa ceara plata (acelasi calcul la agent si la portofel) */ +export function x402Action(walletAddress, resource, req) { + return { kind: 'payment', from: String(walletAddress).toLowerCase(), to: String(req.payTo).toLowerCase(), amount: String(req.amount), + asset: assetId(req.network, req.asset), ref: purchaseRef(resource, req) }; +} +/** nonce-ul EIP-3009 al platii: sha256(hash-ul intrarii), deci plata de pe lant numeste intrarea */ +export const nonceForEntry = (entryHash) => sha(Buffer.from(String(entryHash), 'utf8')); + +/** + * @param {object} c { policy, policyHash, evmPrivateKey, network, token:{address,name,version}, state?, saveState?, now?, toleranceSeconds?, maxValiditySeconds? } + */ +export function createWallet(c) { + const ethers = incarcaEthers(); + const { policy, policyHash } = hashPolicy(c.policy); + if (c.policyHash != null && String(c.policyHash).toLowerCase() !== policyHash) throw new Error('agent-wallet: the policy does not hash to the pinned policyHash'); + const cont = new ethers.Wallet(String(c.evmPrivateKey || '').trim()); + const adresa = cont.address.toLowerCase(); + if (policy.wallet !== adresa) throw new Error('agent-wallet: the policy does not name this wallet (policy.wallet must be its address)'); + if (!/^eip155:[0-9]+$/.test(String(c.network))) throw new Error('agent-wallet: network must be eip155:'); + if (!c.token || !ADRESA.test(String(c.token.address)) || !c.token.name || !c.token.version) throw new Error('agent-wallet: token needs address, name and version (its EIP-712 domain)'); + const network = String(c.network), token = String(c.token.address).toLowerCase(); + const domeniu = { name: String(c.token.name), version: String(c.token.version), chainId: Number(network.split(':')[1]), verifyingContract: ethers.getAddress(token) }; + // un portofel fara limita de cheltuiala in activul lui ar semna orice suma pe care o scrie agentul: nu porneste + if (!policy.spend || policy.spend.asset !== assetId(network, token)) throw new Error(`agent-wallet: the policy needs a spending limit in this wallet's asset (spend.asset = ${assetId(network, token)})`); + const now = c.now || (() => Math.floor(Date.now() / 1000)); + const TOL = Number(c.toleranceSeconds ?? 120); + const VALID = Number(c.maxValiditySeconds ?? 300); + // starea: pe SESIUNE (o sesiune = agentul sub o politica) capetele vazute (ancore) si ultima intrare (pentru dovada de echivocare); + // pe AGENT platile SEMNATE si revocarile primite. Platile semnate raman peste o schimbare de politica: un registru nou (politica noua, + // sesiune noua) incepe de la zero cheltuiala, portofelul nu. Se salveaza INAINTE de a intoarce semnatura. + const stare = c.state || { v: 1, agentId: policy.agentId, sessions: {}, signed: [], revocations: [] }; + if (stare.agentId !== policy.agentId) throw new Error('agent-wallet: the saved state belongs to another agent'); + const sesiune = sessionId(policy.agentId, policyHash); + const S = stare.sessions[sesiune] || (stare.sessions[sesiune] = { anchors: [], last: null, lastEntry: null }); + const salveaza = () => { if (c.saveState) c.saveState(stare); }; + const refuz = (error, extra = {}) => ({ ok: false, error, ...extra }); + + async function autorizeaza({ requirements: req, resource = null, ledger } = {}) { + const t = Number(now()); + // 1. cerinta + if (!req || req.scheme !== 'exact') return refuz('only the exact scheme is supported'); + if (req.network !== network) return refuz(`the requirement is for network ${String(req.network).slice(0, 40)}, this wallet pays on ${network}`); + if (String(req.asset || '').toLowerCase() !== token) return refuz('the requirement is for another asset than this wallet holds'); + if (!ADRESA.test(String(req.payTo || ''))) return refuz('payTo is not an address'); + if (!INTREG.test(String(req.amount || '')) || BigInt(req.amount) === 0n) return refuz('amount is not a positive integer'); + const timeout = Number(req.maxTimeoutSeconds); + if (!Number.isInteger(timeout) || timeout < 10) return refuz('maxTimeoutSeconds is missing or below 10 seconds'); + // 2. registrul, fara incredere, fata de ce a vazut portofelul + if (!ledger || !Array.isArray(ledger.entries) || !ledger.entries.length) return refuz('a ledger with the payment entry is required'); + const v = verifyLedger(ledger, { policy, policyHash, maxTime: t + 5, revocations: stare.revocations, anchors: S.anchors, anchorTolerance: TOL }); + if (!v.ok) { + const r = refuz(`the ledger does not verify${v.seq != null ? ` at seq ${v.seq}` : ''}: ${v.error}`); + // o ramura: intrarea pastrata de la ultimul cap si cea de acum, la aceeasi pozitie, semnate amandoua de agent, sunt dovada + const acum = S.lastEntry && ledger.entries[S.lastEntry.seq]; + if (acum && acum.hash !== S.lastEntry.hash) { + const p = { v: 1, kind: 'aere-agent-equivocation', agentId: ledger.agentId, session: ledger.session, publicKeyPem: ledger.publicKeyPem, + seq: S.lastEntry.seq, a: S.lastEntry, b: acum }; + if (p.agentId === policy.agentId && verifyEquivocation(p).ok) r.equivocation = p; + } + return r; + } + // 3. ultima intrare: plata noua, permisa, legata de cumparatura, scrisa acum + const e = ledger.entries[ledger.entries.length - 1]; + if (S.last && e.seq <= S.last.seq) return refuz(`the last entry (seq ${e.seq}) was already seen; each payment needs a new entry`); + const b = e.body, a = b.action; + if (!b.decision.allowed || a.kind !== 'payment') return refuz('the last entry is not an allowed payment'); + const asteptat = x402Action(adresa, resource, req); + for (const k of ['from', 'to', 'amount', 'asset', 'ref']) if (String(a[k] ?? '').toLowerCase() !== String(asteptat[k]).toLowerCase()) return refuz(`the payment entry names another ${k} than this purchase`); + if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5)) return refuz(`the payment entry is dated ${b.at}, not now (${t}) by this wallet's clock`); + // 4. limita, fata de ce a SEMNAT portofelul (aprobarile intrarii, verificate; refolosirea unei aprobari o prinde deja verifyLedger, + // iar o aprobare e legata de politica, deci nu trece intr-o sesiune noua) + const aprobatori = []; + for (const ap of b.approvals || []) { const r = verifyApproval(ap, { policy, policyHash, action: a, at: Number(b.at) }); if (r.ok) aprobatori.push(r.humanId); } + // (revocarile nu se mai dau aici: verifyLedger le-a judecat deja, iar o revocare e legata de politica, deci nu e alta multime) + const d = checkAction(policy, { ...a, at: Number(b.at) }, stare.signed.map((s) => ({ amount: s.amount, at: s.at })), { approvers: aprobatori }); + if (!d.allowed) return refuz(`by what this wallet has signed: ${d.reason}`); + // semnatura EIP-3009 + const authorization = { from: cont.address, to: ethers.getAddress(req.payTo), value: String(req.amount), validAfter: String(t - 5), + validBefore: String(t + Math.min(timeout, VALID)), nonce: nonceForEntry(e.hash) }; + const signature = await cont.signTypedData(domeniu, TIPURI, { ...authorization, value: BigInt(authorization.value), + validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) }); + S.anchors.push({ seq: e.seq, hash: e.hash, at: t }); + S.last = { seq: e.seq, hash: e.hash }; S.lastEntry = e; + stare.signed.push({ seq: e.seq, entryHash: e.hash, amount: String(req.amount), at: Number(b.at), payTo: authorization.to, nonce: authorization.nonce }); + salveaza(); + const paymentPayload = { x402Version: 2, ...(resource ? { resource } : {}), accepted: req, payload: { signature, authorization }, + extensions: { 'aere-agent-ledger': { agentId: ledger.agentId, policyHash, session: ledger.session, entrySeq: e.seq, entryHash: e.hash } } }; + return { ok: true, paymentPayload, header: Buffer.from(JSON.stringify(paymentPayload), 'utf8').toString('base64'), + receipt: { entrySeq: e.seq, entryHash: e.hash, nonce: authorization.nonce, from: cont.address, to: authorization.to, value: authorization.value }, + decision: decisionEnvelope({ policyHash, action: a, decision: d, createdAt: new Date(t * 1000).toISOString() }) }; + } + // cererile se judeca UNA CATE UNA: verificarea si semnatura au un `await` intre ele, iar doua cereri deodata ar trece amandoua de + // limita inainte ca vreuna sa fie scrisa in stare + let coada = Promise.resolve(); + function authorize(x) { const r = coada.then(() => autorizeaza(x)); coada = r.catch(() => {}); return r; } + function addRevocation(rv) { + const r = verifyRevocation(rv, { policy, policyHash }); + if (!r.ok) return refuz(`revocation refused: ${r.error}`); + stare.revocations.push(rv); salveaza(); + return { ok: true, revokedAt: r.revokedAt }; + } + const status = () => ({ version: VERSION, address: cont.address, network, asset: token, agentId: policy.agentId, policyHash, + lastSeq: S.last ? S.last.seq : null, signed: stare.signed.length, revocations: stare.revocations.length }); + // starea, ca sa fie pastrata peste o schimbare de politica (acelasi agent, acelasi portofel): o copie, nu referinta + const exportState = () => JSON.parse(JSON.stringify(stare)); + return { address: cont.address, network, token, policyHash, authorize, addRevocation, status, exportState }; +} + +// ---------------------------------------------------------------- serviciul HTTP (numai pe 127.0.0.1 implicit) +export function serve(wallet, { port = 8793, host = '127.0.0.1', limit = 8 << 20 } = {}) { + const trimite = (res, cod, o) => { const b = JSON.stringify(o); res.writeHead(cod, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(b) }); res.end(b); }; + const citeste = (req) => new Promise((resolve) => { const parti = []; let n = 0; + req.on('data', (x) => { n += x.length; if (n > limit) { resolve(null); req.destroy(); } else parti.push(x); }); + req.on('end', () => { try { resolve(JSON.parse(Buffer.concat(parti).toString('utf8'))); } catch { resolve(null); } }); }); + const srv = http.createServer(async (req, res) => { + const url = (req.url || '/').split('?')[0]; + try { + if (req.method === 'GET' && url === '/status') return trimite(res, 200, wallet.status()); + if (req.method === 'POST' && (url === '/authorize' || url === '/revocations')) { + const body = await citeste(req); + if (!body) return trimite(res, 400, { ok: false, error: 'invalid or too large JSON' }); + const r = url === '/authorize' ? await wallet.authorize(body) : wallet.addRevocation(body); + return trimite(res, r.ok ? 200 : 403, r); + } + trimite(res, 404, { ok: false, error: 'not found' }); + } catch (e) { trimite(res, 500, { ok: false, error: String(e.message || e).replace(/(0x)?[0-9a-fA-F]{40,}/g, '').slice(0, 200) }); } + }); + return new Promise((resolve) => srv.listen(port, host, () => resolve(srv))); +} + +/** un portofel din fisierul de configurare: cheia EVM citita dintr-un fisier (un singur rand hex sau d=/PRIVATE_KEY=), starea intr-un fisier */ +export function walletFromConfig(file) { + const c = JSON.parse(fs.readFileSync(file, 'utf8')); + const brut = fs.readFileSync(path.resolve(path.dirname(file), c.evmKeyFile), 'utf8').split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + const rand = brut.find((l) => /^(d|PRIVATE_KEY)=/.test(l)) || brut[0] || ''; + const hex = rand.replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim(); + if (!/^[0-9a-fA-F]{1,64}$/.test(hex)) throw new Error('the EVM key file has no hex key (a line of hex, d= or PRIVATE_KEY=0x)'); + const sf = c.stateFile ? path.resolve(path.dirname(file), c.stateFile) : null; + const state = sf && fs.existsSync(sf) ? JSON.parse(fs.readFileSync(sf, 'utf8')) : undefined; + const saveState = sf ? (s) => { const tmp = `${sf}.${process.pid}.tmp`; fs.writeFileSync(tmp, JSON.stringify(s)); fs.renameSync(tmp, sf); } : null; + return createWallet({ ...c, evmPrivateKey: '0x' + hex.padStart(64, '0'), state, saveState }); +} + +if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) { + const a = process.argv.slice(2); const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : undefined; }; + if (a[0] !== 'serve' || !get('--config')) { console.error('usage: node wallet.mjs serve --config wallet.json [--port 8793]'); process.exitCode = 2; } + else { + try { + const w = walletFromConfig(get('--config')); + const port = Number(get('--port') || 8793); + await serve(w, { port }); + const s = w.status(); + console.log(`agent wallet on 127.0.0.1:${port}: ${s.address} pays ${s.asset} on ${s.network} for ${s.agentId} under policy ${s.policyHash}`); + } catch (e) { console.error('agent-wallet: ' + String(e.message || e).replace(/(0x)?[0-9a-fA-F]{40,}/g, '').slice(0, 200)); process.exitCode = 1; } + } +}