An agent gets an ML-DSA-65 identity and a policy (spending per time window, allowed tools and recipients, which actions need human approval, who may revoke it). Every action it proposes is judged against the policy, signed by the agent and chained; a verifier that does not trust the agent re-runs the policy over the whole ledger. Approvals and revocations are signed by people with their own ML-DSA-65 keys. The ledger of an agent under a policy is one ledger: a second history is a branch, and two branches are a proof of equivocation anyone can check with the public key alone. The README says what the verifier cannot see: entry times are bounded from below only with a witness (anchors or a start time), and someone who sees one branch cannot know of another. Tests: policy 23/23 with the AIP-23 reference verifier (21 run without it), ledger 51/51, approval and revocation 39/39, command line 23/23; negative control 25/25.
83 lines
7.5 KiB
Markdown
83 lines
7.5 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. 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>]
|
|
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 }`; every field but `agentId` is optional. 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.
|
|
|
|
## 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 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.
|
|
|
|
## Tests
|
|
|
|
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=<verify-proof.mjs from aere-node>`); without it 21 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 |
|
|
|
|
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.
|