446 lines
63 KiB
Markdown
446 lines
63 KiB
Markdown
# Agents: policy, post-quantum identity, a signed action ledger, human approval and revocation
|
|
|
|
An AI agent that can pay or call tools needs limits that someone other than the agent can check. This component gives an agent a
|
|
post-quantum identity (an ML-DSA-65 key), a policy (a spending limit per time window, allowed tools, allowed recipients, which
|
|
actions need human approval, who may revoke the agent), and a ledger in which every action the agent proposes is judged against the
|
|
policy, signed by the agent and chained. A verifier that does not trust the agent re-runs the policy over the whole ledger and says
|
|
whether any allowed action broke it. Humans approve and revoke with their own ML-DSA-65 keys.
|
|
|
|
Node.js 24 only (`node:crypto` with ML-DSA), no dependencies, no network (except the chain witnesses below, which read a chain, and the
|
|
hosted witness, which is a service). It moves no money: `amount` is the unit the policy names;
|
|
moving value is the job of a payment layer, which can ask for this ledger as evidence that an action was allowed.
|
|
|
|
## Quick start (command line)
|
|
|
|
```
|
|
node agent-cli.mjs id --out agent/ # prints aere-agent:<40 hex>; writes agent.key.pem and agent.pub.pem
|
|
node agent-cli.mjs human --out alice/ # an approver or an owner: aere-human:<40 hex>
|
|
node agent-cli.mjs policy --spec spec.json --out policy.json
|
|
node agent-cli.mjs record --key agent/agent.key.pem --policy policy.json --ledger ledger.json --action pay.json
|
|
node agent-cli.mjs approve --key alice/human.key.pem --policy policy.json --action pay.json --out approval.json
|
|
node agent-cli.mjs record ... --approvals approval.json,approval2.json
|
|
node agent-cli.mjs verify --ledger ledger.json --policy policy.json [--revocations r.json] [--anchors a.json] [--not-before <unix>] [--witness <id>]
|
|
node agent-cli.mjs revoke --key owner/human.key.pem --policy policy.json --out revocation.json
|
|
node agent-cli.mjs equivocation --a ledger-a.json --b ledger-b.json --out proof.json
|
|
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, wallet: 0x<EVM address> }`; 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).
|
|
|
|
## What each part does
|
|
|
|
- **Policy** (`agent-policy.mjs`): `definePolicy` returns the policy in a normal form and its hash. `hashPolicy` recomputes the hash
|
|
of a policy received from anyone and refuses unknown fields, so a relying party that pinned a hash cannot be handed a looser
|
|
policy under it. `checkAction` is pure: a payment must be a strictly positive decimal integer, to an allowed recipient, in the
|
|
policy's asset, and within the limit over the window; a tool must be on the list. Approval is a requirement in addition to the
|
|
limits, never an exemption. `decisionEnvelope` writes a decision as an AIP-23 envelope (`aere-proof-of-agent-decision`) whose
|
|
`actionHash` is the SHA-256 of the action's canonical JSON (keys sorted), so anyone can recompute it.
|
|
- **Identity and ledger** (`agent-ledger.mjs`): `agentId` is derived from the public key (nobody can claim another agent's id). Each
|
|
entry carries the action, the decision, the approvals it used and optionally the hash of its provenance (for example a Proof of
|
|
AI envelope); it is signed by the agent and chained by hash. The spending in the window is derived from the ledger's own allowed
|
|
entries, so the limit holds over the whole ledger, across restarts (`resumeLedger` verifies the saved ledger, then continues it).
|
|
- **Verifier** (`verifyLedger`): recomputes the policy hash, the identity, every signature and the chain, and re-runs the policy
|
|
from the first entry: an "allowed" written over a refusal is caught even when the agent re-signs its whole ledger. Revocations are
|
|
given to it separately, by the owner, because an agent can leave its own revocation out of its ledger; the result says which set
|
|
of revocations it judged against (`revocations.setHash`).
|
|
- **Approval and revocation** (`agent-aprobare.mjs`): an approval signs exactly the action (kind, recipient, amount, asset, tool, and
|
|
the hash of the tool arguments, so an approval to deploy to staging does not approve production), the agent and the policy; it has
|
|
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. With a 2-of-2 contract
|
|
wallet (`AereAgentWallet2of2`, ERC-1271) the money leaves only with the agent's signature and the wallet service's together, so
|
|
neither the agent nor the owner can spend alone. Both were 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`.
|
|
|
|
## The chain as the witness of the ledger (2026-09-30)
|
|
|
|
Without a witness, an entry's time is only the statement of whoever holds the agent key. `agent-ancora.mjs` makes the chain that
|
|
witness: the ledger's head (sequence number and hash) becomes an AIP-23 envelope, its hash is notarized on AereNotary, and the
|
|
first-seen time the contract records, read through the AIP-23 reference verifier under a post-quantum certified anchor (a Merkle
|
|
proof in the certified state), becomes an anchor `{seq, hash, at}` for `verify --anchors`.
|
|
|
|
```
|
|
node agent-ancora.mjs head --ledger ledger.json --out head.json # no network
|
|
node agent-ancora.mjs notarize --head head.json --rpc <url> --key-file <key> --out notarized.json # needs ethers (x402/)
|
|
node agent-ancora.mjs anchors --ledger ledger.json --head notarized.json --rpc <url> \
|
|
--chain 2800 --verify-proof <verify-proof.mjs from aere.network/tools> --out anchors.json # --chain: the chain whose time you accept
|
|
node agent-cli.mjs verify --ledger ledger.json --policy policy.json --anchors anchors.json
|
|
```
|
|
|
|
With the anchor, an entry after the notarized head cannot claim a time before its first-seen time (minus the 300 s tolerance), an
|
|
entry up to the head cannot claim a time after it, and a branch that does not contain the head does not verify. The time is the
|
|
chain's, not the agent's and not the notarizer's: whoever notarizes can only notarize late, which weakens the bound, not move it
|
|
earlier. Only the hash of the head's statement goes on chain, nothing of the ledger. The anchors file is the output of the
|
|
verifier's own run, with the RPC endpoint and the AIP-23 verifier it chose; an anchors file handed over by the party being verified
|
|
proves nothing. `anchors` answers not measured (exit 2) while the finality is pending or when the verifier cannot be run, never an
|
|
anchor it did not certify. `notarize` reads the chain id from the endpoint and refuses chain 2800 (a real transaction paid by the
|
|
key's account) unless `AERE_CONFIRM_MAINNET=yes`.
|
|
|
|
## A hosted witness (2026-10-19)
|
|
|
|
**Sandbox: testnet 28001; mainnet: at activation.** Every answer of the service says so in its `sandbox` field.
|
|
|
|
The section above asks the agent's owner to run a notary transaction, a funded wallet and the AIP-23 verifier. The hosted witness takes the
|
|
first two off the owner: the agent sends its ledger head, signed with its ML-DSA-65 key, and gets back a receipt that anyone can check
|
|
without the witness. `martor-agenti.mjs` is the format, the judge and the client; `martor-agenti-serviciu.mjs` is the service.
|
|
|
|
The form is the one transparency logs already use:
|
|
|
|
- The head is a checkpoint of the agent's log, accepted the way a C2SP tlog-witness accepts a log's checkpoint (`add-checkpoint`): it
|
|
must be signed by the log's key (here the agent's ML-DSA-65 key, from which the agent id is derived), it must name as its predecessor
|
|
exactly the last head the witness holds for that agent and policy (`previous`, `null` for the first; otherwise 409 with that head, as
|
|
C2SP answers 409 with its latest size), and it is written to disk before the answer. The ledger is a hash chain, not a Merkle tree, so
|
|
a consistency proof between two heads would need the entries, that is the actions, which the witness never sees. The agent signs its
|
|
predecessor instead, and the descent itself is checked by whoever holds the ledger: every witnessed head must be an entry of the ledger
|
|
shown.
|
|
- Two heads signed by the agent that give one position two hashes (the same `seq`, or a `previous` against a head already witnessed, also
|
|
when the new head repeats a head the witness holds) are a proof of equivocation (`aere-agent-head-equivocation`): the witness refuses the
|
|
head and returns the proof in the 409. It keeps one proof per agent and policy (one proof is enough to show the equivocation; a further one
|
|
is returned, `kept: false`, not stored). A kept proof belongs to the shard of the head it contradicts and leaves with it, so every kept proof
|
|
has a session of its own with a head in a kept shard: there are at most as many as heads kept (40,000), and that bound is the cap, so nobody
|
|
can fill the store to push out, or keep out, another agent's proof. Until the second review (2026-10-19, R2-02) the cap was 2,000 in all, and
|
|
anyone could fill it with sessions of their own (two requests per session), after which a real equivocation of another agent was returned
|
|
but not kept, and `verify --witness-url` showed that agent clean. The one proof of a session is the one that lives longest: a new proof
|
|
whose contradicted head sits in a NEWER shard than the head of the kept proof takes its place (`replaced` names the proof it took the place
|
|
of, which then answers 404); until the third review (R3-02) an agent could fill the place of its session with an equivocation against a head
|
|
of the old shard, so that the equivocation that mattered was returned not kept, and after the next rotation the session had no proof left.
|
|
`/health` counts the proofs returned and not kept
|
|
(`equivocationsNotKept`: `onePerSession`, `cap`). `GET .../heads` names the kept proofs by id, and `GET .../equivocation/<id>` returns one.
|
|
Anyone checks a proof with the public key inside (`verify-equivocation`).
|
|
- The accepted heads are the leaves of an RFC 9162 log, as in Certificate Transparency and Sigstore Rekor. On receipt the witness
|
|
returns a promise signed with its ML-DSA-65 key that it saw the head at the receive time (the role of an SCT, or of Rekor's signed
|
|
entry timestamp; an AIP-23 `time` envelope). Within the maximum merge delay (RFC 9162's MMD; 1800 s here, checkpoints every 10
|
|
minutes) it signs a checkpoint (the log's name, its size and root, as in C2SP tlog-checkpoint; an AIP-23 envelope of kind
|
|
`aere-proof-of-agent-witness-checkpoint`, registered in `proof-kinds/` with a vocabulary for each field) and notarizes it on AereNotary, so
|
|
the checkpoint's time is the chain's, certified post-quantum through the AIP-23 verifier, not the witness's word. Consecutive checkpoints
|
|
name each other and come with RFC 9162 consistency proofs.
|
|
- The log is **sharded**, the way Rekor and Certificate Transparency shard theirs: a shard holds at most 20,000 heads; when it is full the
|
|
witness signs its final checkpoint and starts the next shard, under a new log name (`aere-agent-witness/28001/<k>`, the first shard keeps
|
|
the plain name), whose first checkpoint names the final checkpoint of the shard before. The witness keeps the current shard and the one
|
|
before; an older shard is removed from its disk and memory, but only once its final checkpoint is notarized: while it is not, a head that
|
|
would make the log rotate gets 503 `log_busy` with `code: notarization_behind` (and `Retry-After`), and the heads of that shard stay served
|
|
(until the second review, R2-07, the shard was removed anyway, and heads answered 202 with a `mergeBy` were left without a certified time).
|
|
A new shard is made whole under a temporary name and then renamed (R2-08: a write that failed between the folder and its `ciob.json`, then a
|
|
restart, used to lose the link between the shards for good). A shard to remove is first renamed (`.sters-<k>-*`, an atomic rename) and only
|
|
then deleted, in the background; the next open deletes whatever carries that name, and a shard older than the two kept that lacks its
|
|
`ciob.json` or its `frunze.ndjson` is taken as the rest of a removal, not as a shard (third review, R3-01: a stop during the background
|
|
deletion used to leave a rest that the opening kept as an unnotarized shard, empty, which no notarization could ever settle, so every new head
|
|
got 503 `notarization_behind` for good, or, with checkpoints left in it, the witness refused to start). Until 2026-10-19 a full log answered 503 to everyone, for good, and anyone
|
|
could fill it.
|
|
- The receipt is that checkpoint, with the head received, its position, the receive time and the RFC 9162 inclusion proof (the role of
|
|
Rekor's `verification.inclusionProof`). It is `VALID` for the AIP-23 reference verifier: form, integrity, the witness's ML-DSA-65
|
|
signature and, once a post-quantum anchor covers the notarization, finality.
|
|
|
|
| route (behind the gateway; the service listens on 127.0.0.1) | what it does |
|
|
|---|---|
|
|
| `POST /v1/agents/witness` | a signed head (`martor-agenti.mjs request`). 202 accepted, with the signed promise; 200 already seen; 409 `equivocation` (the proof, and whether it was kept), `stale_head` (older than the last head witnessed) or `not_successor` (with the last head); 400, 403, 422 a refusal that names its rule (`continut`: any field besides the head and its signature, so a ledger's content is never taken; `semnatura`, `cheie`, `identitate`, `sesiune`, `integritate`, `timp`: a head declared more than 300 s from the witness clock); 413 above 16 KB; 429 above 60 heads per client (an IPv6 client counts by its /64) or 30 changes per agent in 10 minutes (the per-agent count takes only requests whose signature verifies AND that change the state, a new head or a newly kept proof: neither a forged request, nor a replay of the agent's public head, nor a head refused with 503 uses up its quota); 503 `log_busy` above 833 new heads per hour from everyone (20,000 / 24, so a shard never fills in less than a day), or with `code: notarization_behind` while a rotation would remove a shard whose final checkpoint is not notarized, both with `Retry-After` (also through the gateway). The gateway refuses a POST that carries an `Origin` header (a web page, through the open CORS of `/v1/`): a head is sent by the agent, not by a browser |
|
|
| `GET /v1/agents/witness/<id>` | the state of a head (its id is the statementHash of the head): `pending`, `checkpointed` (with an unnotarized receipt) or `notarized` (with the receipt); 404 for a head the witness does not hold, also one of a shard it no longer keeps |
|
|
| `GET /v1/agents/witness/<agentId>/heads?session=<s>[&from=<seq>&limit=<1..100>]` | one page of the witnessed heads of an agent under one policy, in order, with `next` (the seq to ask from, or null), the latest head and the ids of the kept equivocation proofs, and `signedPage`: the same page (each head `{seq, hash, id}`, `next`, `count`, `latest`, the kept proofs) with its time, signed by the witness with its ML-DSA-65 key; nothing is read from disk |
|
|
| `GET /v1/agents/witness/<agentId>/heads[?after=<session>]` | the sessions (policies) of an agent, at most 100 per page, each with its number of heads, its latest head and its number of kept proofs (not signed) |
|
|
| `GET /v1/agents/witness/equivocation/<id>` | one kept proof of equivocation |
|
|
| `GET /v1/agents/witness/checkpoint[?shard=<k>][&from=<size>]` | the latest checkpoint of the current shard, or of the kept shard `k` (for a frozen shard: its final checkpoint), and the latest notarized one, with a consistency proof in that shard from `from`, and the shards kept; a monitor that held a checkpoint of a shard that has since been frozen asks `?shard=<k>&from=<its size>` (until the second review, R2-09, only the current shard was served) |
|
|
| `GET /v1/agents/witness/health` | the witness id, the log and the current shard, `leaves` and `maxLeaves`, the counts (also the proofs not kept, and whether the rotation waits for a notarization), the last checkpoint and the last notarization (a failed notarization shows a code, such as `unfunded`, never its message), and the rate limiters (`limiters`: keys held, the cap, keys dropped at the cap) |
|
|
|
|
The Aere Cloud route `/v1/agents/witness` is not open yet; until it is, a witness can be run by anyone (below), and the commands are the
|
|
same with its address. The id of the hosted witness is published when the route opens; a verifier pins it.
|
|
|
|
```
|
|
node martor-agenti.mjs submit --key agent/agent.key.pem --ledger ledger.json --url <witness> # previous = the witness's last head of this agent
|
|
# nothing new to send: exit 0, no request (2 if the
|
|
# witness then does not give the state of that head);
|
|
# a 200 or 202 that is not the state of the head sent: 2
|
|
node martor-agenti.mjs receipt --url <witness> --id 0x<head id> --ledger ledger.json [--witness aere-witness:<id>] --out receipt.json
|
|
# exit 0 once notarized, 2 before (and on 503,
|
|
# 429, or a 200 that is not JSON); writes the
|
|
# answer ONLY if it is the receipt of that head of
|
|
# that ledger and the witness signature verifies:
|
|
# else 2, the reason named, nothing written
|
|
node agent-cli.mjs verify --ledger ledger.json --policy policy.json --anchors receipt.json \
|
|
--rpc https://testnet-rpc.aere.network --chain 28001 --verify-proof <verify-proof.mjs from aere.network/tools> \
|
|
--witness aere-witness:<id of the witness you trust> [--witness-url <witness>]
|
|
node martor-agenti.mjs verify-receipt --receipt receipt.json --ledger ledger.json [--witness <id>] # the local checks alone, no network;
|
|
# a pending answer: 2; an empty --witness: wrong usage, 2
|
|
node martor-agenti.mjs verify-equivocation --proof proof.json
|
|
node martor-agenti.mjs request --key agent/agent.key.pem --ledger ledger.json --previous <seq>|none --out head.json # the signed head, to POST yourself
|
|
```
|
|
|
|
`receipt` writes what the agent will hand over in `--anchors`, and what the agent hands over is its own responsibility: a receipt it hands over
|
|
that is tied to another ledger, or whose witness signature does not verify, is a fault of the ledger (below). So `receipt` writes the answer (to
|
|
`--out`, or to the standard output) only once it has checked it here: the receipt of the head asked (`--id`), a head of the ledger given
|
|
(`--ledger`: the same agent, policy, seq and hash, and its predecessor), the witness signature verified with the witness pinned by `--witness` when it
|
|
is given, otherwise with the key the receipt names (and the message then says the witness is not pinned), and the other local checks of
|
|
`verify-receipt`. Anything else is exit 2 with the reason named, and nothing is written; `--ledger` is required. So the documented flow (submit,
|
|
`receipt --out`, `verify --anchors`) cannot produce a file that comes out FAILED because of the address it read from (until the sixth review, N2,
|
|
`receipt` wrote whatever the address answered, and a proxy without any key at the agent's `--url` could give an honest agent the true receipt of
|
|
another agent's head, or one with a broken signature, which then came out FAILED when handed over).
|
|
|
|
`verify --anchors` takes a receipt (or the answer of `GET /v1/agents/witness/<id>` as it is) and accepts it as an anchor only after
|
|
checking it here: the witness's signature on the checkpoint, that the witness is one the verifier pinned (`--witness`, repeatable), the
|
|
agent's signature on the head, the leaf recomputed from the head and the receive time, the inclusion proof up to the checkpoint's root, the
|
|
head and its predecessor as entries of the ledger shown, and then the checkpoint judged by the AIP-23 verifier on the chain (`--verify-proof`,
|
|
`--rpc`): the witness's signature and the post-quantum finality `PASSED`, whose first-seen time F must not be before the receive time
|
|
(beyond 300 s) nor later than the maximum merge delay (the witness's and the verifier's, `--max-merge-delay`, 1800 s by default).
|
|
`--chain` and `--max-merge-delay` must be positive integers, and `AERE_WITNESS_WAIT_MAX_S` (below) a whole number of seconds; anything else is
|
|
wrong usage (exit 2). A receipt without `--rpc`, in `--anchors` or taken from the address, is a NOT-MEASURED piece with the rule `rpc`, judged in
|
|
its place after everything that does not need the chain (until the sixth review, N1, a receipt in `--anchors` without `--rpc` was wrong usage,
|
|
returned before the signed list was judged, so any object shaped like a receipt put there by the agent hid a branch the list showed). With
|
|
`--witness-url`, the verifier also reads the full list of heads
|
|
that witness holds for this agent and policy. Only the pages SIGNED by the witness count (`signedPage`, checked against the agent, the policy and
|
|
the page asked, chained by `next`, adding up to the `count` and `latest` it signs), and each must be signed within 300 s of the verifier's clock:
|
|
a server at that address that is not the witness cannot sign the witness's list, and an old page replayed by a proxy is not the list of now
|
|
(until the second review, R2-01, the list and the witness id, read from `/health`, were not signed, so a hostile server at the address passed as
|
|
the pinned witness and hid a head). Without `--witness`, the witness that signs the list is then the pinned one.
|
|
|
|
**`verify` is a complete evaluation followed by one verdict** (fifth review, 2026-10-19; the same text stands beside the code in
|
|
`agent-cli.mjs`). Until then it decided on the way, returning 2 or 1 at the first sign, in the order it read things, and the agent chose that
|
|
order: a fresh head still pending at the witness, a `{"status":"pending"}` put in `--anchors` or an old copy of a receipt hid a backdating that
|
|
the verified receipts proved (R5-01); a list signed by another witness, or a true plain anchor beside the receipts, came out FAILED though
|
|
nothing showed a fault of the ledger (R5-02); and a receipt that did not hold because the notary was late, or because the RPC the verifier
|
|
chose served a wrong header, gave 1 when the agent handed it over and 2 when it was taken from the address (R5-03).
|
|
|
|
1. **Gather.** Every piece of evidence is collected before anything is decided: every entry of `--anchors`, whatever its form; with
|
|
`--witness-url`, the signed list of heads, each head in it, and, for every held head without a VERIFIED receipt in `--anchors` (a receipt
|
|
with the same id that does not hold does not count), its receipt taken from the address. Nothing returns during the gathering. A
|
|
verification with no receipt, no answer of a witness and neither `--witness` nor `--witness-url` does not load the witness module
|
|
(`martor-agenti.mjs`) at all, as before.
|
|
2. **Classify.** Each piece gets one class.
|
|
- VERIFIED: it holds and gives a bound (an anchor of the ledger).
|
|
- LEDGER-FAULT, a fault of the ledger that the evidence PROVES: a head the pinned witness holds that is not an entry of the ledger (a
|
|
branch); a kept proof of equivocation; a receipt of the pinned witness tied to another ledger (another agent, policy or branch, also
|
|
through the signed predecessor of its head); a receipt the AGENT hands over whose signature of the witness it names (a pinned one, or any
|
|
when none is pinned) does not verify; an entry backdated against a bound given by VERIFIED evidence (and any other refusal of the ledger
|
|
itself by `verifyLedger`). The two kinds of receipt the agent answers for are the ones `receipt` no longer writes (above, since the sixth
|
|
review, N2): a file `receipt` wrote for this ledger is not one of them, whatever the address answered.
|
|
- NOT-MEASURED: anything else. A head or receipt pending or not notarized yet; a wrong form or wrong types; a receipt judged without `--rpc`
|
|
(`[rpc]`); a notary that is behind (`[intarziere]`); a finality the AIP-23 verifier refuses or leaves pending through the RPC the verifier
|
|
chose (`[lant]`); a receipt or a
|
|
list of another witness than the pinned one (`[martor]`); a plain anchor under a pinned witness (`[martor]`); an answer the address breaks,
|
|
a receipt with a broken signature taken from the address included (an address cannot make a FAILED); `--witness` with no receipt of a
|
|
pinned witness and no list (`[martor]`).
|
|
3. **Verdict.** FAILED (exit 1) if there is at least one LEDGER-FAULT; otherwise NOT MEASURED (exit 2) if a REQUIRED piece is NOT-MEASURED;
|
|
otherwise OK (exit 0). Every piece is required except a receipt, or an answer without a receipt, in `--anchors` whose head has a VERIFIED
|
|
receipt of its own: that old copy is listed as superseded. So a proven fault cannot be hidden by a piece that could not be measured, however
|
|
early it comes, and an outage of the witness, its notary or the verifier's RPC cannot turn an honest ledger into a FAILED. The order in which
|
|
the pieces come does not change the verdict: the negative control reverses it in three ways (the held heads before `--anchors`, `--anchors`
|
|
from the last, the held heads from the last) and requires the whole probe to stay green.
|
|
|
|
Rule (c), the types: the type of every value that comes from outside (`--anchors`, the address, the service) is judged before it is used. A
|
|
negative `receivedAtMs`, an object (`{"toString":0}`) where a string or a number is expected, in a receipt, in a page of the list, in a `status`
|
|
or in the answers to `submit`, is a NOT-MEASURED piece with its rule named (`chitanta-forma`, `punct-integritate`, `lant`, a page refused by its
|
|
form), or for `submit` exit 2 with `--out` not written, never an exception (until the fifth review, R5-04, these came out 1 through an
|
|
exception; on the "nothing new" path of `submit`, the unsigned `latest` of the page and the state of the head it names, until the sixth, N3). The output: one line per piece on stderr (`ok` verified, `FAIL` a fault of the ledger, `--` not measured and required, `..`
|
|
superseded) and a closing line; the JSON result carries `verdict` (`OK`, `FAILED`, `NOT MEASURED`), `ok` (true, false, null) and `evidence`
|
|
(each piece with its `source`, `class` `VERIFIED`, `LEDGER-FAULT` or `NOT-MEASURED`, `required`, `seq` and `rule`).
|
|
|
|
The class of an entry of `--anchors`, by its form:
|
|
|
|
| an entry of `--anchors` | no witness pinned | `--witness` or `--witness-url` |
|
|
|---|---|---|
|
|
| a receipt, or a GET answer that carries one, that holds here and on the chain | VERIFIED; its witness is not pinned, so the chain alone bounds the head, and the result says so | VERIFIED if its witness is a pinned one; a receipt of another witness, its signature valid: NOT-MEASURED `[martor]` |
|
|
| a receipt whose signature of the witness it names does not verify (`receipt` does not write one since the sixth review, N2) | LEDGER-FAULT (`[punct-semnatura]`, or `[martor]` for a key that is not the key of that witness) | LEDGER-FAULT when it names a pinned witness |
|
|
| a receipt of the witness for another ledger: another agent, policy or branch, or a head whose signed predecessor is not an entry (`receipt` does not write one for the ledger it is given since the sixth review, N2) | LEDGER-FAULT `[registru]` | LEDGER-FAULT `[registru]` |
|
|
| a receipt whose inclusion proof, leaf, promise, receive rule or head signature fails, the witness signature valid | NOT-MEASURED, by the rule that fails | the same |
|
|
| a receipt not notarized yet, notarized too late (`[intarziere]`), with a finality refused or pending (`[lant]`), with no AIP-23 verifier, or judged without `--rpc` (`[rpc]`; until the sixth review, N1, wrong usage, returned before the list was judged) | NOT-MEASURED | the same |
|
|
| a receipt with wrong types (`receivedAtMs` -1, an object for a hash or a chain id) | NOT-MEASURED `[chitanta-forma]` or the rule of the field | the same |
|
|
| an answer of the witness without a receipt (`{"status":"pending"}`, `checkpointed`) | NOT-MEASURED `[fara-chitanta]` | the same |
|
|
| a plain anchor `{seq, hash, at}` | VERIFIED, as the verifier's own anchor (anchors of your own run, such as `agent-ancora.mjs anchors`); one that is not an entry of the ledger: LEDGER-FAULT `[ancora]` | NOT-MEASURED `[martor]`: the word of whoever wrote the file, not the witness's (until the fifth review, R5-02, FAILED) |
|
|
| anything else: null, a number, a string, an object of no known form | NOT-MEASURED `[forma]` | the same |
|
|
|
|
A receipt taken from `--witness-url` is classified the same way, except that a broken signature of the witness is NOT-MEASURED there (the
|
|
address, not the agent, gave it). Anchors of your own run are judged in a run without `--witness` and `--witness-url`; each anchor only adds
|
|
constraints, so two runs catch what one would.
|
|
|
|
So an honest ledger whose last head the witness notarized late (a notary outage beyond the merge delay) is not measured, not refused, whether
|
|
the agent hands over that receipt or the verifier takes it (until the fourth review, R4-01, it came out FAILED when taken, and until the fifth,
|
|
R5-03, when handed over); a proxy at the address that breaks the answer for one head gives not measured, never FAILED and never an exception
|
|
(R4-02, R5-04); and an agent that hands over only the receipt of an old head does not leave the entries after an INTERMEDIATE head backdatable
|
|
down to the old one (R4-03), whatever else it puts in `--anchors` (R5-01). With `--witness-url` and a witness that holds no head of the ledger,
|
|
the list is judged and a NOTE says that the time of the entries is not bounded by the witness. Without `--witness-url` the bound is still the
|
|
one of the receipts the agent hands over.
|
|
|
|
**The cost, and the read limit of the witness** (fifth review, R5-06). The verifier takes one receipt per held head that has no verified receipt
|
|
in `--anchors`, and runs the AIP-23 verifier once per checkpoint (the heads of one round share it). The service gives 1,200 reads per client in 10
|
|
minutes, the pages of the list included (a page holds 100 heads). A read over the limit answers 429 with `Retry-After`, the seconds until the
|
|
client's window opens again, and the verifier waits as asked, within one budget for the whole run: `AERE_WITNESS_WAIT_MAX_S`, 900 s by default,
|
|
named in a NOTE when it was used. Measured on 2026-10-19 with the real limits: an honest ledger with 1,250 held heads and no receipt handed over,
|
|
exit 0 after waiting 595 s (601 s in all; until this review, exit 2 at the head seq 1,187, whose read was refused with 429). By arithmetic, not measured: the default budget covers
|
|
two windows of reads (one wait of at most 600 s), so about 2,370 held heads; each further 1,200 heads ask up to 600 s more of budget, and a run
|
|
whose budget is spent is NOT MEASURED, with the 429, its `Retry-After` and the seconds already waited named. An agent that hands over its own
|
|
receipts spares those reads. The gateway passes `Retry-After` through (read in its code, not measured live); the limits of the public door
|
|
itself (the gateway and its edge) were not measured.
|
|
|
|
**A head sent for an older entry is a statement about now** (fifth review, a finding rejected by its verifier, kept here as the intended
|
|
behaviour). A head says "my head was `seq` at `createdAt`". An agent that comes back after a time offline and sends, one by one, heads for
|
|
entries it wrote meanwhile (`submit --seq <k>` for each), signs for each an old position at a NEW time; the entries after it, written before,
|
|
then declare times before that head, and `verify --witness-url`, which takes every head the witness holds as a bound, gives FAILED `backdated`
|
|
(without `--witness-url`, with the receipts of older heads only, 0). The signed statement is false, and nothing tells it apart from a real
|
|
backdating, so FAILED is the honest answer. The recovery that does not produce it is `submit` without `--seq`, which sends the current tip.
|
|
|
|
The arguments of both command lines come in pairs, a flag and its value. A flag that takes a value and is given without one (the last on the
|
|
line, or followed by another flag) or with an empty value, a flag the command does not know (a typo such as `--ledgr`, or the form
|
|
`--flag=value`), and a flag given twice (but `--witness`, which repeats) are wrong usage (exit 2), in `agent-cli.mjs` and in
|
|
`martor-agenti.mjs` (third review, R3-03, and fourth, R4-05: `--witness-url` or `--policy-hash` put last, `--revocations ""`,
|
|
`verify-receipt --ledger ""`, `--witness-url=<URL>`, `--policy-hash=<h>`, `verify-receipt --witness=<id>` or `--ledgr` used to drop the list
|
|
of the witness, the pinned policy, the revocations, the pinned witness or the link of the receipt to the ledger, without a word); only
|
|
`--reason` may be empty. The result names the witness of every receipt (`witnessReceipts`, with `pinned`), and when a witness was not pinned its
|
|
`time.lowerBound` says the bound is the chain's alone.
|
|
|
|
An end-to-end run with a witness of your own on testnet 28001 (Node.js 24; ethers for the notarization: `npm install` in `x402/`):
|
|
|
|
```
|
|
node agent-cli.mjs id --out agent/
|
|
node agent-cli.mjs policy --spec spec.json --out policy.json
|
|
node agent-cli.mjs record --key agent/agent.key.pem --policy policy.json --ledger ledger.json --action pay.json
|
|
node martor-agenti-serviciu.mjs --stare witness/ --port 8802 --cheie testnet.key \
|
|
[--robinet <client of the testnet faucet>] # notarizes on testnet 28001 every 10 minutes (--interval); --fara-lant: no chain at all
|
|
node martor-agenti.mjs submit --key agent/agent.key.pem --ledger ledger.json --url http://127.0.0.1:8802
|
|
node martor-agenti.mjs receipt --url http://127.0.0.1:8802 --id 0x<id from submit> --ledger ledger.json --out receipt.json # after the next checkpoint
|
|
node agent-cli.mjs verify --ledger ledger.json --policy policy.json --anchors receipt.json \
|
|
--rpc https://testnet-rpc.aere.network --chain 28001 --verify-proof verify-proof.mjs --witness-url http://127.0.0.1:8802
|
|
```
|
|
|
|
`--cheie` is a testnet key file (a hex key, or a `d=<hex>` line); if it does not exist the service makes one there (mode 0600) and, given
|
|
`--robinet`, funds it from the public testnet faucet; it never prints it. The service reads the chain id from `--rpc` and refuses any chain
|
|
but 28001 before it touches the key.
|
|
|
|
**What a receipt proves.** That a head of the agent's ledger (its sequence number and entry hash), signed by the agent, was in the log of
|
|
the witness that signed the receipt no later than a time the chain certifies post-quantum (the checkpoint's first-seen time on testnet
|
|
28001), with its inclusion checkable by anyone from the receipt alone. As an anchor: the agent signed that its head was `seq` at
|
|
`createdAt`, so an entry written after the head that declares a time before `createdAt` minus 300 s contradicts the agent's own signature
|
|
and is caught as backdated, and an entry up to the head cannot declare a time after it. The chain bounds `createdAt` to between F minus the
|
|
merge delay and F, each widened by 600 s (the witness's 300 s rule between `createdAt` and the receive time, plus 300 s between the receive
|
|
time and F). **Only a witness the verifier pins** (`--witness`, or the one at `--witness-url`) tightens it to 300 s around its clock and
|
|
refuses an equivocating branch: any ML-DSA-65 key can be a witness of itself, so an agent can run its own witness, set its clock back to
|
|
F minus 2,100 s, and obtain a receipt for a head claimed at F minus 2,400 s, or for a branch the hosted witness refused as an equivocation.
|
|
Without pinning such a receipt is still accepted, as the agent's own signed statement bounded by the chain, and the result says so.
|
|
|
|
**What it does not prove.**
|
|
|
|
- The witness never sees, and never judges, the actions: it receives a sequence number and a hash, and refuses any other field. Whether an
|
|
action was allowed is the ledger verifier's job (`verify`), with the ledger.
|
|
- The lower bound on the time of the entries after a head rests on the clock of a pinned witness up to 300 s; beyond that it is the chain's,
|
|
within the merge delay. Without `--witness-url` it holds only after the heads whose receipts the agent hands over; with it, after every head
|
|
the witness holds (the complete evaluation above), and a head held with no verified receipt makes the verdict not measured, unless the
|
|
evidence proves a fault of the ledger. A witness that colludes with the agent (or a witness the agent runs, when the verifier pins none) can accept a head
|
|
claimed up to the merge delay earlier than the truth.
|
|
- The witness checks that a new head names its last head as the predecessor, not that it descends from it (that needs the entries). A
|
|
branch that forks after the last head for which the agent hands over a receipt passes the receipts: it is caught only with the full list
|
|
of heads the witness holds (`verify --witness-url`), not with the receipts the agent chooses to give. The list is signed by the witness page by
|
|
page; what the signature does not cover is the order of the requests: a page is fresh for 300 s, so a proxy that recorded a page can replay it
|
|
within those 300 s, and a branch whose head reached the witness within the last 300 s may be hidden that way. A witness that lies in its own
|
|
signed list is the witness lying, which no list can catch (C2SP cosigning by other witnesses is not built here, below).
|
|
- The witness's memory is two shards (at most 40,000 heads; at the hourly cap, at least the last day), and a third only while the final
|
|
checkpoint of the oldest one waits for its notarization. A head of an older shard is no longer served (`GET` answers 404; its receipt, once
|
|
notarized, stays verifiable without the witness), and the witness no longer remembers it: an agent whose last head was there starts again with
|
|
`previous` null, and an equivocation against a head the witness no longer holds is not caught by the witness, nor kept (a kept proof leaves
|
|
with the shard of the head it contradicts; receipts already given still show the equivocation to whoever holds both).
|
|
- Sandbox: checkpoints are notarized on the public testnet 28001 with a test key; nothing here runs on chain 2800 (mainnet at activation).
|
|
- The availability of a witness is not proven: a witness that is down or that drops a head breaks only its promise, which the agent
|
|
holds signed. A flood can delay new heads for everyone (503 `log_busy` above the hourly cap, 429 per client); it cannot fill the log for
|
|
good, and it cannot lose a head already answered. A notarization that fails for as long as a shard takes to fill (at least a day at the
|
|
cap) stops new heads (503 `notarization_behind`) until it succeeds again: the log waits rather than drop heads it promised to anchor. Each rate
|
|
limiter keeps at most 100,000 client keys; at that cap the key with the oldest window is dropped and the new client gets in (until the second
|
|
review, R2-03, every NEW client was refused while the cap was full, so whoever filled 100,000 keys locked out every new client). A flood
|
|
from more than 100,000 keys in 10 minutes therefore makes the per-client count partial (a dropped client starts a fresh window), never a
|
|
lockout; `/health` shows the keys held and dropped. Whether the Cloudflare edge delivers IPv6 client addresses to the witness is not
|
|
measured; the per-client count groups an IPv6 address by its /64 either way.
|
|
- The log refuses to start on a damaged state: a leaf line damaged in the middle (not a line cut at the tail, which is never answered and
|
|
is cut), a leaf without its head, or a checkpoint signed over more leaves than are on disk or with another root (the witness would
|
|
otherwise sign a fork of its own log). The service then stays down until a person looks, so whoever runs it should alarm on its unit
|
|
stopping. A write that fails halfway is cut back before the answer, so it does not leave such a line. The lock of the state (`.lacat`)
|
|
carries the process id, the start of the machine (`boot_id` on Linux; elsewhere the boot time from `os.uptime`, within 120 s) and, where
|
|
`/proc` has it, the start time of the process: a lock left by an unclean stop (a power cut, a reset) whose process id now belongs to another
|
|
live process no longer keeps the witness from starting (until the second review, R2-10, it did, at every restart). On a system without
|
|
`/proc` (Windows, the laboratory) a lock of the same boot whose process id was reused by another live process is still taken as held.
|
|
- One witness is not a network of witnesses: someone who sees only one witness cannot know what another was shown; C2SP-style
|
|
cosigning of the witness's checkpoints by other witnesses is not built here.
|
|
|
|
**Bounds**, derived from measurement (2026-10-19, Node.js 24.14.1, Windows; `martor-agenti.mjs` writes the arithmetic beside each constant):
|
|
|
|
- Shards: at most 20,000 heads each, two kept (a third only while its final checkpoint waits for the notarization). At the hourly cap of 833
|
|
new heads (20,000 / 24) a shard takes at least a day to fill, so a head is served, and remembered by the witness, for at least a day after its
|
|
shard is frozen.
|
|
- The WORST state, measured after the second review (2026-10-19, R2-02 and R2-06): both shards full (40,000 heads), one checkpoint per head (low
|
|
traffic gives that: at most one checkpoint per round, whenever a head came) and one kept proof of equivocation per head (a session per head:
|
|
the bound of the proofs by construction). The first review's bounds were measured with one checkpoint every 1,000 heads and 2,000 proofs at
|
|
most, and missed this state: with one checkpoint per head, the memory per interval of the Merkle tree (rebuilt at every open, and growing with
|
|
every receipt served from a frozen shard) took ~5 KB per checkpoint, about 372 MB at the open (measured by a verifier), and above the unit's
|
|
cap of then (`MemoryMax=400M`) once the limiters were full. The log's tree is now incremental (only the perfect subtrees, 32 bytes each, at most
|
|
2n per shard; no memory per interval), checked against `merkleTree` of `aere-verify-layer` on every size up to 70 and every pair of sizes up to 40.
|
|
- Disk, measured on that state: 1,320 MB (33 KB per head: the signed head and promise ~15.5 KB, the leaf line 0.35 KB, a checkpoint ~7.9 KB, a kept
|
|
proof ~7.9 KB, which holds only the new head: the head it contradicts is already in its shard). The installer asks for 2,048 MB free (~1.55x).
|
|
- Memory, measured on that state in a fresh process: the open peaks at 266 MB of RSS (50 MB of it the empty process), serving 2,000 receipts,
|
|
2,000 signed pages of heads, 2,000 kept proofs and a consistency proof of the frozen shard peaks at 277 MB, and with the three rate limiters at
|
|
their key cap (100,000 keys each) at 330 MB (314 MB after a collection). The unit's `MemoryMax` is 512M (~1.55x), and the installer asks the host
|
|
for at least that much available memory. A state with one checkpoint per 1,000 heads now retains ~340 bytes of heap per head (before: ~900).
|
|
- Time: the open of the worst state took 37 s on the laptop (reading 40,000 checkpoints and 40,000 proofs; on Linux not measured). Signing a page
|
|
of 100 heads takes ~1.1 ms. Per-request latency was measured only before the second review (no submission above 200 ms, rotations included);
|
|
the worst-state run, with a checkpoint built after every head and fsync off, averaged 8.6 ms per head. A shard is removed from disk after the
|
|
answer, in the background. A page of `GET .../heads` reads nothing from disk.
|
|
|
|
**Going back to a version before the second or the third review** (the deployer's rollback restores the unit and the tools, not the state): the state
|
|
format stays `aere-agent-witness-state-2`, so the version before opens it; what differs is this. Kept proofs are now written to `echivocari-2/`
|
|
(only the new head); the version before reads only `echivocari/`, so it does not see them (and does not delete them), and they are back when
|
|
this version runs again, unless the head they contradict was removed meanwhile. Proofs of the version before are read by this one, tied to the
|
|
shard of the head they contradict, and dropped when that head is no longer kept. The lock is JSON now; the version before reads it as no
|
|
process, removes it and writes its own. The version before removes, at its first open, a shard older than the two newest even when this version
|
|
kept it waiting for its notarization, and its heads then lose their certified time, as they did before the review. Going back also puts back the
|
|
unit of before (`MemoryMax=400M`); on a state with one checkpoint per head the version before needed ~372 MB at its open (measured by a verifier
|
|
of the second review) plus ~60 MB of limiters at their cap, above that cap. Since the third review (R3-01) a removed shard is renamed
|
|
`cioburi/.sters-<k>-*` before it is deleted, and, when the rename fails, marked with a `.de-sters` file: the versions before do not know either.
|
|
They ignore a `.sters-*` folder (not a shard name), which then stays on disk until this version opens the state again; and a shard folder marked
|
|
`.de-sters` whose deletion was interrupted, or a rest without `ciob.json`, they keep as a shard (the defect R3-01 repairs). A proof that replaced
|
|
another (R3-02) is an ordinary proof of form 2 for them.
|
|
|
|
## 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
|
|
bounds it from below only with a witness: `anchors` (ledger heads seen by a witness at a known time, for example notarized) or
|
|
`notBefore`. Without one, the key holder can backdate entries into past windows and spend the limit several times; the result
|
|
says so (`time.lowerBound: "none..."`). With anchors, backdating is limited to the interval between two anchors; the chain itself
|
|
can be that witness (above). The library
|
|
itself refuses to write an entry more than 300 seconds away from its clock.
|
|
- **One ledger per agent and policy.** The ledger's session is derived from the agent and the policy hash, so a "second ledger" is
|
|
a branch of the same one. Two branches signed by the agent are a proof of equivocation (`findEquivocation`, `verifyEquivocation`)
|
|
that anyone can check with the public key alone: this is how a double spend or an approval used twice across branches is shown.
|
|
Someone who sees only one branch cannot know that another exists; anchors or a second copy reveal it. An agent that loses its
|
|
ledger cannot continue it without looking like a branch: keep the ledger durable, or start under a new policy.
|
|
- **Revocation time** is the owner's statement, as the agent's entry times are the agent's.
|
|
- Nothing here proves that the actions happened in the world, only what the agent recorded and whether the policy allowed it.
|
|
- **Corrected on 2026-10-03: the agent key must be an ML-DSA-65 key.** Until then `verifyLedger` did not require it: the agent id
|
|
is derived from any key and the signature check accepts whatever key it is given, so a ledger written and signed with an Ed25519
|
|
key verified as the ledger of a post-quantum identity, and `openLedger` opened one for such a key. Found while this verifier was
|
|
enrolled in the conformance suite (`conformance/`), where it is now a vector. Both now refuse by name. Every refusal of
|
|
`verifyLedger` also carries a rule id from a declared list (`REGULI`, the field `rule`).
|
|
|
|
## Tests
|
|
|
|
Measured on 2026-09-29 (Node.js 24.14.1, Windows):
|
|
|
|
| test | result |
|
|
|---|---|
|
|
| `node proba-agent-policy.mjs` | 27/27 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools>`); without it 25 run, 2 are reported as skipped and the exit code is 2 |
|
|
| `node proba-agent-ledger.mjs` | 59/59 (measured 2026-10-19; since then an equivocation proof must be made with the agent's ML-DSA-65 key, AIP-25 finding C2): a ledger written and signed with a classical key refused by name, by the verifier and by the writer; 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 proba-agent-ancora.mjs` | 26/26 without a network (the AIP-23 verifier's answer in its own form): the head, the time read from the finality level (pending, failed, a mismatched or unknown form: no anchor), an envelope that claims another time, backdating caught with the anchor and not without it, another branch, the refusals before any transaction (a modified head, chain 2800 without confirmation, no contract at the notary) |
|
|
| `AERE_TESTNET_KEY_FILE=<funded testnet key> node proba-agent-ancora-testnet.mjs` | 7/7 on the public testnet 28001, measured 2026-09-30: a real ledger's head notarized, read back under a post-quantum certified anchor by the published AIP-23 verifier within 20 s, the certified time equal to the contract's first-seen time, the honest continuation valid with the anchor, a backdated continuation valid without it and caught with it, another branch refused; evidence in `dovezi-ancora-28001/` |
|
|
| `node control-negativ-ancora.mjs` | 11/11: each anchoring guard removed in a copy turns its named test red |
|
|
| `node control-negativ-aprobare.mjs` | 28/28 (2026-10-19): 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 proba-martor-agenti.mjs` | 158/158 offline, measured 2026-10-19 after the sixth review of the hosted witness (two reviewers and two verifiers in each of the first two, a reviewer and a verifier in the third, the fourth and the fifth, and as the sixth a targeted verification of the fifth's repairs; 18 findings in the first, all confirmed; 15 confirmed and one rejected in the second; 5 confirmed in the third; 6 confirmed and one rejected in the fourth; 6 confirmed and one rejected in the fifth; 4 in the sixth, N1..N4, of which N4 is this table; until the sixth review this row said 138/138, "after the fourth"), with keys made at run time and a local notary mock named as such: two agents, successive heads, a head that skips, an older head and one that does not continue the last (409), equivocation at the same position and through `previous` caught and returned, one proof kept per agent and policy, the named refusals of a request (form, the ledger's content, signature, key, identity, session, integrity, time), the rate limits, checkpoints signed by the witness with inclusion and consistency proofs, a restart on the same state, a leaf line cut at the tail by a crash, any chain but 28001 refused before the key, receipts checked without the witness and as anchors (backdating caught with the anchor and not without it), the command line, the gateway's door with and without `AERE_AGENTS_WITNESS_PORT` (in the development repository; the gateway is not part of this copy, where those rows are information), the checkpoint `VALID` for the AIP-23 reference verifier (`AERE_VERIFY_PROOF`; without it those rows are skipped by name and the exit code is 2); and a row for each finding (`9.R<n>`): a pinned witness and the receipt of a witness the agent runs itself, the full list of heads (`--witness-url`), the sharded log and the hourly cap, an equivocation on the "already seen" path, `--chain 2800x`, `submit` with nothing new, the vocabulary of the checkpoint kind, the cap on kept proofs, the paginated `GET .../heads`, a replay of the agent's public head, the limiters (IPv6 /64, key cap and cleanup) and the door's refusal of a browser `Origin`, short and failed writes, a damaged state refused at start, error codes instead of messages; and a row for each finding of the second review (`10.R2-<nn>`, with `10.P2-<nn>` the controls of the guards its repairs added): the list of heads signed by the witness (a hostile proxy that drops a head, one that changes only the unsigned fields, an impostor, an old page replayed), kept proofs tied to the shard of the head they contradict (a stranger who fills the cap no longer keeps out another agent's proof), the limiter at its key cap, a list with malformed elements, plain anchors and an empty anchors file under a pinned witness, the incremental Merkle tree against `merkleTree` and the heap of a state with one checkpoint per head, the rotation that waits for a notarization, a shard born whole and one repaired, the route to a frozen shard, the lock after a reboot, the exit codes of `receipt` and `verify-receipt`, the log budget read before the agent quota, `Retry-After` through the door, and the "root" half of the opening guard. Run on a copy with the files of before the first review and its probe, 22 of its 23 rows failed (the 23rd, `9.P11`, is the control of a guard the repair added, which the old code passed for another reason); run on a copy with the files of before the second review (cf56bcda6) and this probe, 21 of the 24 new rows fail, each on its scenario, and the other 3 (`10.R2-15` twice, `10.P2-13`) pass there too: the behaviour was already right, what was missing was a test that could turn red; and a row for each finding of the third review (`11.R3-<nn>`, with `11.P3-<nn>` the controls of its new guards): the rest of a shard removal interrupted by a stop (in any order, and a rest of the form before, left in place), the one proof of a session that lives longest, flags given without a value, the receipt of the last head the witness holds under `--witness-url` (and a pending one: exit 2), and the exit codes of `receipt` and `submit` on answers that are not the state of a head. Run on a copy with the files of c34b34f1b and this probe, 7 of the 8 new rows fail, each on its scenario, and the 122 rows of before pass; the 8th, `11.P3-04b`, gives the check of the list against the ledger a row of its own, which the receipt of the last head (R3-04) can no longer hide: it passes there too; and a row for each finding of the fourth review (`12.R4-<nn>`, `12.P4-<nn>`), on the rule of `--witness-url`: a receipt notarized late taken from the address (not measured, not FAILED), a proxy that breaks the answer for one held head (not measured, no exception), a receipt for EVERY held head (an intermediate one too), a held head whose signed predecessor is not in the ledger (FAILED), `submit` on a 200 or 202 that is not the state of the head sent, flags written `--flag=value`, mistyped or given twice, the choice between two proofs of a session at the opening, and the `.de-sters` mark. Run on a copy with the files of c5183bcb8 and this probe, 5 of the 8 new rows fail, each on its scenario, and the 130 rows of before pass; the other 3 (`12.P4-03b` and the two `12.R4-06`) give their own row to guards that were right there, which is what R4-06 found missing; and a row for each finding of the fifth review (`13.R5-<nn>`, `13.P5-<nn>`), `verify` as a complete evaluation followed by one verdict: a head still pending, a `{"status":"pending"}` or an old copy of a receipt beside a backdating the verified receipts prove, a list signed by another witness and a plain anchor under a pinned one, a notary outage and a finality refused through the verifier's RPC on both paths, wrong types without an exception, a receipt of the agent that does not hold beside a pending head in both orders, and the wait on a 429. Run by the verifier of those repairs on a copy with the files of b55038c2a and that probe (no gateway in the copy), 129/150: 15 of the 16 new rows fail, each on its scenario, and so do 6 rows of before whose expected verdict the fifth review changed; `13.P5-01` passes there too; and a row for each finding of the sixth (`14.N<n>`): a receipt without `--rpc` beside a branch the signed list shows (not measured `[rpc]`, the branch FAILED), `receipt` through a proxy without any key (the true receipt of another agent's head, a broken witness signature, the receipt of another head of the same ledger) and against `--ledger` and `--witness`, and wrong types on the "nothing new" path of `submit`. Run on a copy with the files of 786aa0038 and this probe (no gateway in the copy, so its 4 rows are information), 149/154: the 4 new rows fail, `14.N1` and `14.N3` on their scenarios and the two `14.N2` rows already at `--ledger`, a flag that version refuses (that `receipt` through such a proxy exited 0 and wrote the file there was measured by the targeted verification), and so does `10.R2-11`, which now calls `receipt` with `--ledger` too; the other rows of before pass |
|
|
| `node proba-martor-agenti.mjs --testnet` | 75/75 on the public testnet 28001, measured 2026-10-19 at 05:35 UTC with the version BEFORE the adversarial review (the 70 offline rows of that version plus 5): a fresh test key funded from the faucet, a checkpoint notarized on AereNotary (block 6,828,096), the receipt `VALID` for the AIP-23 reference verifier with the witness's signature and the post-quantum finality `PASSED` (51 s after the notarization), `verify --anchors receipt.json` on the live chain: the honest ledger 0, the backdated one 1. Evidence in `dovezi-martor-agenti-28001/` (no private key). **The versions after the first and the second review were not run on the testnet** (not measured: the faucet allows one claim per 24 h, and it was cooling down); their testnet rows also pin the witness (`--witness`), with the receipt that pinning now requires. Without a funded key and a faucet that answers, the testnet rows are skipped by name (exit 2) |
|
|
| `node control-negativ-martor-agenti.mjs` | 131/131 plantations red on their row and 3/3 neutral ones green (2026-10-19, after the sixth review; 1,102 s with 12 plantations at a time, the default; after the fourth review, with 112 plantations, ~475 s with 12 and ~747 s with 4, `AERE_CONTROL_PARALEL=4`; the B2 runner gives this test 1650 s since the sixth review, 1.5 x the measurement on an idle laptop, after the 1200 s of before stopped a green control at 1,212 s inside the suite while another workload held the processor (the arithmetic is in `probele-b2.sh`); until the sixth review this row said 112/112 "after the fourth", and the fifth's 123 plantations and 3 neutral ones were not written here): every one of the 99 guard markers (`PAZA-MARTOR`) of the witness, the service and, since the second review, the command line (`agent-cli.mjs`) has a plantation that removes it, and a coverage gate counts a marker without one as a failure of the control (with its own check: one plantation taken out, the gate names its marker); the guards the repairs added (the per-agent quota after "already seen", the "already seen" exit after the predecessor check, the write to the last byte and the cut-back, the paging, the /64 key and the key cleanup, the error codes, the command line's pinned witnesses, `--chain` and an unanswering `--witness-url`, the vocabulary in `proof-kinds/`, the door's `Origin` refusal) are planted too. Each removal is made in a copy, and the named row must fail; a pattern not found once, or a test that stops before its summary, counts as a failure of the control. The guards of the second review are planted too (the signed page: its form, integrity, signature, age and count, and reading it instead of the unsigned fields; the list signed by another witness than the pinned one; proofs tied to the shard of the head they contradict and the cap derived from the shards; the limiter dropping its oldest key; a malformed list; plain anchors and no receipt under a pinned witness; the rotation and the opening that keep an unnotarized shard; the shard born whole and the link repaired; the route to a kept shard; the lock across a reboot and a reused process id; the exit codes; the budget read before the quota; `Retry-After` through the door), and the opening guard is split into its size and its root, each with its row. So are those of the third (the rename before a shard is deleted and the removal of a rest at the opening; the newer proof taking the place of the older; a flag without a value, in both command lines; the receipt of the last head under `--witness-url`; the exit codes of `receipt` and `submit`), and those of the fourth (every held head with its receipt, the receipt of a head required, its id read without an exception, a receipt taken from the address judged as testimony both ways, `submit` on the main path, unknown and repeated flags in both command lines, the choice between two proofs at the opening, and the two halves of the rest of a removal, each with its row). Those of the fifth review are planted too (the order of the classes in the verdict, an early exit on the way, the class of each receipt both ways, the old copy, the types, the wait on a 429), with three NEUTRAL plantations that reverse the order in which the evidence comes and require the whole probe to stay green; and those of the sixth (a receipt without `--rpc` as a not-measured piece, planted both ways: the early exit of before and the guard removed; `receipt` writing only a verified receipt of the head asked, with its id, its ledger and its pinned witness each planted; the types on the "nothing new" path of `submit`). After the first review the control planted 64 guards over 45 markers and did not look at `agent-cli.mjs`; until the first review it planted 24, which covered 20 of the 34 markers of then |
|
|
| `bash ../../cloud-gateway/proba-desfasoara-martor-agenti.sh` (development repository) | 12/12 (2026-10-19): the rollback functions and the laboratory copy of the deployer, extracted from its host script and run on the laptop with `systemctl` replaced: a reinstall that fails before step 5 leaves the live service and its unit untouched, one that fails after it puts the old unit back and restarts the service, a first install that fails is removed, the laboratory is beside the state (not under `/root`) and removed on the way back, a laboratory copy of a real state opens with the same leaves; and as negative control the rollback of before the review (it stopped the live service and deleted its unit) and five plantations, each red on its scenario |
|
|
|
|
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.
|