provared 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,691 +2,154 @@
2
2
 
3
3
  Proof of what an AI agent was allowed to do, and what it then did.
4
4
 
5
+ A person signs the permission with a passkey. The agent signs a receipt
6
+ for each action. A free checker says whether the agent stayed within the
7
+ permission, and where it did not.
8
+
5
9
  *Evidence, not a verdict.* A record shows what was signed and by which
6
10
  keys. People decide what it means.
7
11
 
8
- ## Status
9
-
10
- **Version 0.2.0, a draft.** Published as it is, by one author in their
11
- own time; fixes as time allows. The format may still change, so do not
12
- rely on a draft record staying checkable.
13
-
14
- The core of the format is also written as an individual Internet-Draft
15
- at the IETF:
16
- [draft-izmaylov-agent-permission-receipts](https://datatracker.ietf.org/doc/draft-izmaylov-agent-permission-receipts/).
17
- An Internet-Draft is a working document, not a standard.
18
-
19
- ## The four parts
20
-
21
- | Part | What it is | In this draft |
22
- |---|---|---|
23
- | **Slip** | A short permission a person signs with a passkey: which agent, which actions, what limits and conditions, with whom, from when until when, and why | Built |
24
- | **Stub** | A signed receipt for each action, pointing back to its slip, countersigned by the other side where possible, and approved by the person where the slip asks for it | Built |
25
- | **Seal** | The stubs chained together and gathered under one top fingerprint, signed by whoever keeps the book and time-stamped by an outside service | Built. A public place where seals are kept is not |
26
- | **Show** | One page shown to a checker with proof that it is in the book, and when the book was sealed, with the names it does not need kept covered | Built. Only a slip's names and purpose can be covered so far |
27
-
28
- ## What is here
29
-
30
- | Folder | What it holds |
31
- |---|---|
32
- | `spec/provared-format.md` | The format, described exactly |
33
- | `docs/threat-model.md` | Who might attack a record, what each part protects, and what it does not |
34
- | `src/` | The library: the checker, the writers for every kind of record, the stub writer beside an agent and the connector for an agent's tools. No outside code |
35
- | `bin/provared-check.mjs` | The checker for the command line |
36
- | `bin/provared-headers.mjs`, `net/` | A command that fetches the chain of Bitcoin block headers from nodes you name, so that a block time-stamp can be checked with no node of your own. The only code here that makes a network request. Experimental |
37
- | `page/` | The checking page and the demonstration page |
38
- | `demo/` | The demonstration: an invented office agent works under a permission and goes outside it four times |
39
- | `samples/` | A sample record, written once, that the tests keep checking |
40
- | `tools/` | Three small tools: one wrote the sample record, one writes the checking page as a single file, one writes the shared test files |
41
- | `test/` | The tests |
42
- | `test-vectors/` | Shared test files: records and values, each with the answer this library gives. Another implementation of the format can be tested against them |
43
- | `python/` | The Python version, `pip install provared`, which gives the same answers. See its own [README](python/README.md), and its examples with three agent frameworks in `python/examples/` |
44
-
45
- The npm package holds `src/`, `bin/`, `net/`, `spec/`, `docs/`,
46
- `samples/` and the checking page. The tests, the shared test files, the
47
- demonstration, the tools and the Python version are in the repository
48
- only.
49
-
50
- ## Install
51
-
52
12
  ```
53
- npm install provared
13
+ npm install provared # Node.js 24.7 or later, no dependencies
14
+ pip install provared # Python 3.11 or later, one dependency
15
+ npx provared-check --sample # the checker's answer for the sample record
54
16
  ```
55
17
 
56
- You need Node.js 24.7 or later. There are no dependencies. Node.js marks
57
- its ML-DSA and SLH-DSA as experimental and may print a warning about
58
- them; the command-line checker does not repeat it.
18
+ The sample is an invented office agent that goes over its limit at its
19
+ fifth order. The checker's answer ends:
59
20
 
60
- ```js
61
- import { checkBook, openRecorder } from 'provared'; // the whole library
62
- import { checkBook } from 'provared/check'; // the checker alone, as a page uses it
63
- import { createPasskey } from 'provared/passkey-browser'; // a passkey, in a browser
64
21
  ```
65
-
66
- The checker for the command line is `provared-check`:
67
-
68
- ```
69
- npx provared-check record.jsonl --issuer <the thumbprint of the passkey you trust>
22
+ Summary
23
+ Is the record intact? yes
24
+ Was every signature checked? yes
25
+ Did the agent stay within its slip? NO: first at entry 5
26
+ Time-stamped in a way you trust? yes: the first 9 entries existed by 2026-10-05T10:16:00Z
27
+ Checked against keys you named? yes: the passkey you trust, and the recorder you expect
70
28
  ```
71
29
 
72
- The examples below import from the package by name. In a copy of the
73
- repository, import from `./src/index.js`, `./src/check.js` and
74
- `./src/passkey-browser.js` instead.
75
-
76
- ## Try it
77
-
78
- These need a copy of the repository. You need Node.js 24.7 or later.
79
- Nothing is installed: there are no dependencies.
80
-
81
- **Run the tests.**
82
-
83
- ```
84
- npm test
85
- ```
30
+ ## A first record of your own
86
31
 
87
- **Check the sample record from the command line.**
32
+ Three commands. The permission is signed with a **development stand-in
33
+ for a passkey**: a key made in memory, which no person confirmed. Every
34
+ permission it signs says so in its issuer's name, and the checker prints
35
+ that name. A real permission is signed by a person, in a browser, with a
36
+ real passkey (`provared/passkey-browser`, and the checking page).
88
37
 
89
38
  ```
90
- npm run check -- samples/office-supplies.jsonl
91
- ```
92
-
93
- It prints each entry, then its answers: is the record intact, was
94
- everything checked on this device, did the agent stay within its slip, is
95
- it time-stamped in a way you trust, and was it checked against keys you
96
- named. Here you named none, so it is not a pass, whatever else it shows;
97
- the fingerprints that come with the sample are given below.
98
- In the sample the agent goes outside its slip four times: an order the
99
- supplier never confirmed, an order above 60 pounds that the person was
100
- not asked to approve, a total of 230 against a limit of 200, and a
101
- deletion that the slip forbids. The checker says where.
102
-
103
- **Make the checking page as one file.**
104
-
105
- ```
106
- node tools/make-single-page.mjs
107
- ```
108
-
109
- It writes `provared-check.html`: the checking page, its style, the
110
- checker and the sample record in one file, copied in unchanged. Open it
111
- in a browser straight from the disk; it needs no server. The file allows
112
- itself no request of any kind: its content security policy lets nothing
113
- be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
114
- so that a copy can be compared with it. (Tried on 8 October 2026 in the
115
- three browser builds this repository is tested with.) The file for
116
- version 0.2.0 is attached to the release on GitHub. Its SHA-256
117
- fingerprint is `7e142fa119458ce00c5fdd6e1cafc75cb3bb4672624157a0a0ef4624f92812ae`.
118
-
119
- **Run the demonstration in a browser.**
120
-
121
- ```
122
- npm run demo
123
- ```
124
-
125
- Then open `http://localhost:8787`. You sign a permission with a passkey,
126
- the agent works and stops once to ask your approval of one order, and the
127
- page shows where the agent went outside the permission. Everything in it
128
- is invented: nothing is bought, sent or deleted. The checking
129
- page on its own is at `http://localhost:8787/check`. It takes a record
130
- file, the fingerprints you already trust (keys, time-stamp services,
131
- blocks), and what you were handed with the record: the person's own
132
- copies of cancellations, and disclosures for covered fields. All of it is
133
- read in the browser and sent nowhere.
134
-
135
- ## What a slip can say
136
-
137
- - **What the agent may do**: a list of action names. Use names of your
138
- own, or names from the shared list in the format description, section
139
- 16, such as `provared.order.place`, so that another organisation's
140
- checker knows what is meant.
141
- - **Limits**: a total, a limit on each single action, a number of
142
- actions, and a total or a number within any period.
143
- - **Conditions**: that the other side must countersign an action, and
144
- that the person must approve an action with their passkey, always or
145
- above an amount.
146
- - **Stated rules**: kinds of action the agent must never do, such as
147
- `destroys`, and rules of conduct, such as "never claim to be a human
148
- being". A checker compares the first with the stubs. The second is the
149
- person's signed instruction: no check can show that the agent kept it.
150
- - **With whom**: the services the agent may deal with, and their keys.
151
-
152
- The other side has a voice of its own. A service can sign a **refusal**
153
- ("this agent asked for something outside its slip, and we refused") and
154
- its **terms for agents** (which actions it accepts). Both are shown as
155
- what the service said.
156
-
157
- ## The seal and the outside time-stamp
158
-
159
- Whoever keeps the book seals it from time to time: they sign its top
160
- fingerprint and its number of entries, with three signatures (Ed25519,
161
- ML-DSA-87 and SLH-DSA-SHA2-256s). An outside time-stamp service then
162
- states when that seal existed, in the standard form of RFC 3161.
163
-
164
- - After a seal by a recorder you named (`--sealer`, or `sealKeys`),
165
- nothing before it can be changed, added or removed without the seal no
166
- longer fitting. A seal by keys you did not name shows only that someone
167
- sealed the book as it now stands.
168
- - With a time-stamp from a service you trust, the entries before the seal
169
- existed by the time the service states. An entry dated later than that
170
- is reported.
171
- - **You say whom you trust.** This format names no time-stamp service.
172
- You give the checker the fingerprint of the certificate of each service
173
- you trust; a time-stamp from any other service is shown, and not
174
- counted. In the same way you can give the fingerprint of the key set of
175
- the recorder you expect.
176
-
177
- - **A second clock that rests on no signature.** Beside a time-stamp
178
- from a service, a seal can carry a **block time-stamp**: proof that
179
- its fingerprint was written into a block of a public blockchain (the
180
- open proof format OpenTimestamps, and the Bitcoin blockchain). The
181
- chain is used purely as a clock: no coin, no wallet, no account and no
182
- payment is involved. It stays good even if every signing method of
183
- today is broken. You name the blocks you trust by their fingerprints
184
- (`--block`, or `blocks`). A block's time is right only to within about
185
- two hours, and the checker says so. The tests use made-up blocks. Two
186
- complete proofs that others had published, one made by hand and one
187
- made by public calendar services, were checked once with this reader
188
- against the real blocks they name (5 October 2026): each led to its
189
- blocks, and each was refused with another block or with its file
190
- changed by one bit.
191
-
192
- ```
193
- npm run check -- samples/office-supplies.jsonl --stamp-service <fingerprint> --sealer <fingerprint>
194
- ```
195
-
196
- The fingerprints for the sample are in
197
- `samples/office-supplies.expected.json`. The sample's time-stamp service
198
- is made up, as its passkey is.
199
-
200
- ### Knowing that a block is part of the chain
201
-
202
- **Experimental.** `provared-headers` is new in version 0.1.0 and may
203
- change. It is the only part of Provared that connects to the internet,
204
- and it does so only when you run it.
205
-
206
- A block time-stamp counts only for a block you trust to be part of the
207
- Bitcoin blockchain. You need not run a Bitcoin node of your own, or trust
208
- anyone who lists blocks, to know that:
209
-
210
- ```
211
- provared-headers --node <host:port> --node <host:port>
212
- provared-check record.jsonl --headers block-headers.bin --issuer <thumbprint> --sealer <fingerprint>
213
- ```
214
-
215
- `provared-headers` fetches the chain of block headers (about 78 MB, a
216
- few minutes the first time) from the Bitcoin nodes you name, and from no
217
- one else, and keeps it in a file. It checks every header on your device,
218
- by the rules every Bitcoin node applies, from the chain's first block:
219
- each names the block before it, carries the work its target asks for,
220
- follows the rule for the difficulty, and states a time that the rules
221
- allow. The chain must also hold, at their places, ten blocks known to be
222
- part of it, which are written into the library, and after the last of
223
- them no target may be more than four times easier than that block's. A
224
- second node, on another network, must hand back the latest six blocks
225
- itself. A node can refuse to answer, but cannot have a false header
226
- kept. A chain file someone else hands you is only as good as they are:
227
- fetch your own. Run it again later and it
228
- fetches only the newer headers. Name your own node if you have one
229
- (`127.0.0.1:8333`); a file of nodes, one `host:port` to a line, can be
230
- given with `--nodes-file`. The nodes you name see your device's address,
231
- and a node named by its name is looked up by your device's own name
232
- service.
233
-
234
- `provared-check --headers` checks that file again, in full, on your
235
- device, and makes no request. A block time-stamp then counts if its block
236
- is in the chain with at least six blocks after it. The checker says, for
237
- each block time-stamp, what the chain shows. In the library:
238
- `checkHeaderChain` and `blocksInChain`.
239
-
240
- ## Cancelling a slip
241
-
242
- The person who signed a slip can end it early with the same passkey, and
243
- have the cancellation time-stamped at once. Every stub after it is then
244
- reported as outside the slip. With a time-stamp on the cancellation, a
245
- stub counts as made before it only if an outside time-stamp shows that
246
- it existed by then: an agent cannot escape a cancellation by writing an
247
- earlier time into a stub. If the person cancelled more than once, every
248
- cancellation counts, in whichever order they stand in the book.
249
-
250
- The person should keep their own copy of a cancellation, with its
251
- time-stamp. Whoever keeps the book could leave the cancellation out, or
252
- put a later time-stamp in its place. Handed to the checker beside the
253
- book (`--cancellation <file>`, or `cancellations`), the person's copy
254
- counts all the same: the checker says whether the book holds it, and
255
- reports every stub that no outside time-stamp shows to have existed
256
- before it.
257
-
258
- A cancellation shows when the person signed it, not that the agent's
259
- side was told. So the agent's side can **acknowledge** it: a short
260
- record, signed with the agent's own keys, that says "this cancellation
261
- was handed to us at this time". The stub writer signs one whenever the
262
- person's cancellation of its slip is handed to its `add`, once for each
263
- cancellation, writes it into the book after the cancellation, and hands
264
- it back for the person to keep. Handed to the checker with the person's
265
- copy (as `acknowledgements` in the same file), it shows which stubs the
266
- agent made after it was told, which passes it handed on after it was
267
- told, and whether a cancellation that the book leaves out had been
268
- handed over. Both dates in that comparison are the agent's own word. An
269
- acknowledgement that does not check is reported, and the cancellation
270
- itself is still used.
271
-
272
- ## Who stands behind a name
273
-
274
- A name in a record is a label. An organisation can stand behind one with
275
- a **vouching record**: "this key belongs to this name". It can vouch for
276
- a person's passkey, an agent's keys, a service's keys and a recorder's
277
- keys, and can later **withdraw** the statement. You tell the checker
278
- which organisations you trust (`--voucher`, or `vouchers`), and it then
279
- shows a name as "vouched for by" that organisation instead of "a label,
280
- not checked". The vouching record must be in force for the whole of the
281
- slip's time. Provared does not decide who anyone is: it records who
282
- vouched.
283
-
284
- A slip can also state the fingerprints of the agent's program and
285
- settings, as the person approved them. That is a statement, not proof of
286
- what ran.
287
-
288
- ## Covered fields
289
-
290
- A slip can be written with its names and its purpose covered: the person's
291
- name, the agent's name, the services' names and the purpose. What is
292
- signed then holds a fingerprint in place of each. To let a reader see a
293
- field, hand over its **disclosure**; to keep it hidden, do not. The
294
- signature, the chain and the tree check either way, and a disclosure
295
- cannot be made for a value that was not signed. A disclosure that does
296
- not fit is refused and said to be, and the slip is checked as it would
297
- be without it: what a slip is does not depend on what is handed over
298
- with it. Nothing that a checker
299
- compares can be covered: not an action, a limit, a condition, a service
300
- or a key. So every reader checks the same slip.
301
-
302
- ```js
303
- const prepared = await prepareSlip(fields, { cover: ['issuer.name', 'purpose'] });
304
- prepared.disclosures; // keep these beside the slip
305
-
306
- await checkBook(book, { disclosures: { [fingerprintOfTheSlip]: theDisclosuresYouWereHanded } });
307
- await makeShow(book, [0, 3], { disclosures: { 0: theOnesThisReaderMaySee } });
308
- ```
309
-
310
- The method is the standard "Selective Disclosure for JSON Web Tokens"
311
- (RFC 9901), which digital identity wallets also use. The standard's own
312
- worked example is one of the tests. A slip uses less than the standard
313
- allows: no covered item of a list, no decoy fingerprints, and
314
- disclosures written in one canonical form.
315
-
316
- ## Passing a slip on
317
-
318
- A slip can say that its agent may hand part of its permission to a helper
319
- agent, and how many times in a row (`passes`: from 1 to 10). The agent then
320
- signs a **pass** that names the helper's keys, what the helper may do,
321
- its limits and its times. A helper's stubs name the pass, are signed with
322
- the helper's keys, and are compared with the pass, with every pass above
323
- it, and with the slip the person signed. The totals of the slip, and of
324
- each pass, take in everything done under them, so passing on cannot
325
- widen a permission. A pass the slip does
326
- not allow is reported, with every stub under it.
327
-
328
- ## The check before acting
329
-
330
- `checkBefore` answers, before an action: would this be outside the slip?
331
- It uses the same comparison as the checker, so it refuses everything the
332
- checker would report afterwards. In one case it refuses more: an action
333
- that would add to a period which a stub dated after it already
334
- overfills; the checker reports that against the later stub. The software
335
- around an agent can call it and stop the action.
336
-
337
- ```js
338
- import { checkBefore } from 'provared';
339
-
340
- const answer = await checkBefore(
341
- textOfTheBookSoFar,
342
- {
343
- slip: fingerprintOfTheSlip,
344
- action: 'provared.order.place',
345
- amount: { unit: 'GBP', value: 80 },
346
- with: 'supplier',
347
- approval: theSignedApprovalIfTheSlipAsksForOne,
348
- },
349
- { issuerKeys: ['the thumbprint of the passkey you trust'] },
350
- );
351
-
352
- answer.allowed; // true only if the record so far is sound, fully checked, and the action is inside the slip
353
- answer.breaches; // why not, in the checker's own codes
354
- answer.problems; // why the question could not be answered with a yes
355
- answer.needs; // what the slip asks to go with it: 'approval', 'countersignature'
356
- ```
357
-
358
- It must be told whose passkey it trusts: a slip signed by any other key
359
- allows nothing. Where the slip asks for the person's approval, the action
360
- is allowed only if the signed approval is handed over. Where the slip
361
- asks for a countersignature, `needs` says so: the action is inside the
362
- slip only if the other side then countersigns it.
363
-
364
- It is a help, not evidence: nothing in a record shows that it was called.
365
-
366
- ## Beside an agent
367
-
368
- `openRecorder` puts the two things an agent's software must do into one
369
- call: ask first, and write the receipt after.
370
-
371
- ```js
372
- import { openRecorder } from 'provared';
373
-
374
- const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
375
-
376
- const outcome = await recorder.act(
377
- { action: 'provared.order.place', amount: { unit: 'GBP', value: 45 }, with: 'supplier' },
378
- async () => placeTheOrder(), // taken only if the answer is yes
379
- { countersign: askTheSupplierToCountersign },
380
- );
381
-
382
- outcome.done; // false: the action was outside the slip, was not taken, and nothing was written
383
- recorder.book(); // the book, with the new stub in the agent's chain
384
- await recorder.check(); // what the record shows as it stands, as checkBook would answer
39
+ npm install provared
40
+ node first-record.mjs
41
+ npx provared-check book.jsonl --issuer <the thumbprint it prints>
385
42
  ```
386
43
 
387
- If taking the action fails with an error, no stub is written; the caller
388
- must deal with whatever part of the action did happen. `record` writes a
389
- stub without asking, for an action that was taken some other way: the
390
- record is of what happened, inside the slip or not.
391
-
392
- The writer's calls wait for one another, so two actions asked for at the
393
- same moment are taken one after the other, each against the book as the
394
- one before left it. A countersignature that the other side hands back is
395
- checked before it goes into the book; one that does not check is left
396
- out, the stub stands one-sided, and `stub.countersignature.problem` says
397
- why. `add` puts an entry that someone else made (a seal, a cancellation,
398
- a refusal) into the book only if the book still passes its check with
399
- it, and it is not dated ahead of the clock. The other side is given 30
400
- seconds to countersign (`countersignWithin`); after that the stub is
401
- written one-sided. Where the slip asks for a countersignature, `act`
402
- takes the action only if it is given a way to ask for one. Before an
403
- action is taken, its stub is written and checked with the whole book, so
404
- that the receipt is known to fit. `act` takes an action now: a date
405
- handed to it must be within 300 seconds of the writer's clock (`now`).
406
- Nothing is written that is dated more than 300 seconds ahead of that
407
- clock. What the writer is handed (the request, an approval, what the
408
- other side hands back) is copied when it is handed over, and the copy is
409
- what is checked and written. `record` hands a stub to the other side
410
- only once it has passed the check of the whole book. The function
411
- that takes the action must not call the writer. Use one writer for one
412
- chain: two writers on two copies of a book know nothing of each other.
413
-
414
- The writer reads its book once, when it is opened, and then carries it
415
- on: each new line is read by itself, and no signature is checked twice.
416
- So the time an action takes grows only slowly with the length of the
417
- book. Measured on one computer on 4 October 2026, with countersigned
418
- stubs: 9, 14 and 23 thousandths of a second for one action after 400,
419
- 1,600 and 6,400 stubs. (Before the book was carried on: 2.0 seconds
420
- after 400 stubs and 7.7 after 1,600.) Opening the writer still reads
421
- the whole book: about one second for every 450 stubs. Every line
422
- is tried on a reader split off the book's own before it joins the book,
423
- so an entry that does not fit, or an error part of the way through a
424
- call, leaves the writer's view of its book exact. `recorder.check()`
425
- gives the checker's answer for the book as it stands, without reading
426
- it again. `before` waits its turn with the other calls. What the writer
427
- is told to trust (`options`) is read once, when it is opened, and
428
- fixed.
429
-
430
- A cancellation that the person signed is never lost in `add`. If it
431
- cannot be added as it was handed over (a faulty time-stamp beside it, a
432
- member that cannot be written as a line, such as a list with a gap in
433
- it), the writer adds the cancellation with its time-stamps, or with
434
- those of them that pass their check, or by itself. If it cannot be added even so (it is dated ahead
435
- of the clock), the writer keeps it, allows nothing more under the slip,
436
- and writes it into the book, at its next call, once the clock has
437
- reached its date. In both cases `add` fails, and the error says what was
438
- done. (Where the clock fails, the error is the clock's own; the
439
- cancellation is held all the same, as `cancellations()` shows.) A
440
- cancellation that the writer keeps lives only in that writer:
441
- `recorder.cancellations()` hands back what it holds, each with the
442
- acknowledgement the writer signed for it, and so does an acknowledgement
443
- that still waits to follow its cancellation into the book. Keep that
444
- with the book, and hand it in again (`options.cancellations`, an empty
445
- list included) when you open a writer again; a writer opened from the
446
- book alone knows nothing of it. Read `recorder.book()` and
447
- `recorder.cancellations()` together, with nothing awaited in between,
448
- so that the two you keep belong to the same moment. A cancellation given in that way is written into the book once
449
- its date has come, and stops the writer from the start. A copy that
450
- the writer holds takes the time-stamps handed over with it later: each
451
- is judged by itself, those the writer counts come first, and a copy
452
- holds four at most. The writer is stopped before it reads its clock, so
453
- a clock that fails then does not lose the cancellation, nor the sound
454
- time-stamps, nor the acknowledgements handed over with it. A clock that
455
- gives anything but a time counts as a clock that failed. An
456
- acknowledgement that is handed over with a cancellation is taken as the
457
- agent's own only if it checks by itself, and only the first such one is
458
- taken: the writer never puts a second one of the agent's own with a
459
- copy (a copy given at opening that already holds two is handed back as
460
- it was given). A copy holds four
461
- acknowledgements at most; the writer's own is always among them, and
462
- where the copy already holds four, the last of the others gives way to
463
- it. A writer that is stopped
464
- takes no action, but `record` still writes the stub of an action that
465
- was taken all the same: the record is of what happened, and a checker
466
- reports such a stub.
467
-
468
- ### The agent's tools
469
-
470
- An agent acts by calling tools: functions that its software offers it.
471
- `recordTools` puts each tool behind the stub writer, so that no call is
472
- forgotten. The agent's software calls the tools as before.
44
+ `first-record.mjs` ([examples/first-record.mjs](examples/first-record.mjs)):
473
45
 
474
46
  ```js
475
- import { openRecorder, recordTools } from 'provared';
476
-
477
- const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
478
- const tools = recordTools(recorder, {
479
- placeOrder: {
480
- action: 'provared.order.place', // the name the slip uses for it
481
- with: 'supplier',
482
- amount: (order) => ({ unit: 'GBP', value: order.total }),
483
- run: (order) => sendTheOrder(order), // run only if the slip allows it
484
- countersign: (stub) => askTheSupplierToCountersign(stub),
485
- },
486
- readStock: { action: 'provared.data.read', run: (query) => lookUpStock(query) },
487
- });
488
-
489
- await tools.placeOrder({ item: 'paper', boxes: 5, total: 45 });
490
- ```
491
-
492
- A call that the slip does not allow throws `NotTaken`, with the answer
493
- of the check before acting, and the tool is not run. Where the slip asks
494
- for the person's own approval, a tool's `approve` function is asked for
495
- it once. By default the stub of a call names one document,
496
- `arguments`: the fingerprint of the call's arguments, written in one
497
- form (RFC 8785). So the record fixes exactly what was asked for without
498
- holding it; `argumentsFingerprint` works the same fingerprint out again.
499
- The arguments must be JSON: a list with a gap in it, or with a named
500
- member, or a member that is `undefined`, is refused, and the tool is
501
- not run. A fingerprint of arguments
502
- that are few and easy to guess can be guessed. What a tool is, and what
503
- it is called with, is copied when it is handed over; each function a
504
- tool is described by gets a copy of the arguments of its own, and is
505
- called by itself, without `this`: it is not handed the description and
506
- cannot change it. What `amount`, `with` and `details` give is
507
- copied too, so a function that keeps what it gave cannot change it
508
- afterwards. `amount` and `with` must give their value at once,
509
- not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
510
- 8785 writes both as `0`. If `onStub` fails, its error is passed on,
511
- though the tool has run and its stub is in the book. `recordTools`
512
- names no agent software, and works with any function.
513
-
514
- ## How signing works
47
+ import { writeFileSync } from 'node:fs';
48
+ import { developmentRecorder } from 'provared/dev';
515
49
 
516
- - **The person** signs a slip with a passkey. A passkey signs a short
517
- value handed to it; that value is the SHA-256 fingerprint of the slip's
518
- exact signed bytes, so the signature covers the whole slip.
519
- - **The agent and the service** each sign with two keys at once: Ed25519,
520
- which is today's kind, and ML-DSA-87, which is quantum-safe. A record
521
- counts only if both signatures check. A forger must break both.
522
- - **The passkey signature is of today's kind only**, because no device
523
- offers a quantum-safe passkey yet.
524
- - **Whoever keeps the book** signs each seal with three keys: the same
525
- two, and SLH-DSA-SHA2-256s, which rests only on fingerprint functions.
526
- - **Browsers are only now adding ML-DSA, and have no SLH-DSA.** Where a
527
- browser lacks a method, the checking page says that those signatures
528
- were not checked, and does not show a pass. Node.js checks all of them.
50
+ const SEND = 'provared.message.send';
529
51
 
530
- ## Using the library
531
-
532
- ```js
533
- import { checkBook } from 'provared/check';
534
-
535
- const result = await checkBook(textOfTheBook, {
536
- issuerKeys: ['the thumbprint of an issuer key you already trust'],
52
+ // A permission: the agent may send messages, at most two.
53
+ const { recorder, issuerKeys } = await developmentRecorder({
54
+ actions: [SEND],
55
+ limits: [{ action: SEND, count: 2 }],
56
+ purpose: 'Send a few messages, to make a first record.',
537
57
  });
538
58
 
539
- result.summary.intact; // no problem found, and every signature checked
540
- result.summary.problemFound; // was a problem found in the record?
541
- result.summary.fullyChecked; // was every signature checked on this device?
542
- result.summary.withinSlips; // every stub compared with its slip, none outside it
543
- result.summary.firstBreach; // the first entry that shows the agent outside its slip
544
- ```
545
-
546
- Type declarations come with the library (`src/types.d.ts`), so an editor
547
- can check how it is called.
548
-
549
- **A book that grows.** `checkBook` reads a whole book each time. To
550
- follow a book as lines are added to it, open a checker once and carry it
551
- on. It reads each new line against what it worked out from the lines
552
- before it, and checks no signature twice. Its answer is the answer
553
- `checkBook` gives for the same text; a test compares the two at every
554
- length of a book that holds every kind of entry.
555
-
556
- ```js
557
- import { openChecker } from 'provared/check';
59
+ // Each action is asked for first, taken only if the permission allows it,
60
+ // and receipted after. The third message is outside it, so it is not sent.
61
+ for (let i = 1; i <= 3; i++) {
62
+ const outcome = await recorder.act({ action: SEND }, async () => console.log(`message ${i} sent`));
63
+ if (!outcome.done) console.log(`message ${i} not sent: ${outcome.answer.breaches[0].message}`);
64
+ }
558
65
 
559
- const checker = await openChecker(textOfTheBookSoFar, { issuerKeys });
560
- await checker.add(newLines); // whole lines, each ending with a line feed
561
- const result = await checker.result(); // the same shape as checkBook returns
66
+ writeFileSync('book.jsonl', recorder.book());
67
+ console.log(`npx provared-check book.jsonl --issuer ${issuerKeys[0]}`);
562
68
  ```
563
69
 
564
- What the checker is told to trust is fixed when it is opened. Each answer
565
- is a copy of its own.
566
-
567
- The checker never throws for a bad record. A failed check is a normal
568
- answer with a named reason. It keeps two things apart:
70
+ The same in Python is
71
+ [python/examples/first_record.py](python/examples/first_record.py), with
72
+ `from provared.dev import development_recorder`. For a LangChain agent,
73
+ `pip install provared-langchain` puts every tool call behind the same
74
+ check with no change to the tools
75
+ ([python/integrations/provared-langchain](python/integrations/provared-langchain)).
569
76
 
570
- - **problems**: reasons the record cannot be relied on (a changed entry, a
571
- signature that does not fit, a broken chain);
572
- - **breaches**: what a sound record shows about the agent (an action the
573
- slip does not allow, a limit passed).
574
-
575
- ### What the checker returns
77
+ ## The four parts
576
78
 
577
- | Member | Meaning |
79
+ | Part | What it is |
578
80
  |---|---|
579
- | `size`, `root` | How many entries the book holds, and its top fingerprint |
580
- | `problems` | Problems with the book as a whole |
581
- | `entries` | One for each line: `index`, `kind` (`slip`, `stub`, `refusal`, `terms`, `seal`, `cancellation`, `acknowledgement`, `vouching`, `withdrawal`, `pass` or `unreadable`), `fingerprint`, `verified` (`all`, `some` or `none` of its signatures confirmed here), `problems`, `breaches`, `notes`, the signature states, and `content`. Where a time-stamp you trust on a later seal covers the entry: `existedBy`, the time by which it is shown to have existed. On a seal or a cancellation that passed its check: `stampedAt`, the time its own time-stamps give it |
582
- | `held` | One for each cancellation handed over beside the book: whether it passed its check and was used, whether the book holds it (`inBook`), the time its time-stamp gives, and the acknowledgements handed over with it (`acknowledgements`: who acknowledged, and when by their own word) |
583
- | `summary.problemFound` | A problem was found somewhere in the record |
584
- | `summary.fullyChecked` | Every signature was checked on this device |
585
- | `summary.intact` | No problem was found **and** every signature was checked. The only answer that is a pass |
586
- | `summary.methodsMissing` | The signing methods this device lacks, if any, for example `ML-DSA-87` |
587
- | `summary.withinSlips` | Every stub was compared with its slip, and none is outside it |
588
- | `summary.firstBreach` | The first entry that shows the agent outside its slip, if any |
589
- | `summary.counts` | Slips, stubs, countersigned stubs (the countersignature confirmed on this device), one-sided stubs (no countersignature; a stub that was not checked, because its slip failed, is neither), stubs the person approved, refusals, sets of terms, and the other kinds of entry |
590
- | `summary.sealed` | How far the last time-stamp you trust reaches: how many entries, the time they existed by, and whether a service or a block (`by`) gave it. Otherwise nothing |
591
- | `summary.limits` | What no check can show, in plain sentences |
592
-
593
- A signature state is `valid`, `invalid`, `unavailable` (this device
594
- lacks the method; never a pass), or `unchecked` (not checked, because
595
- what it rests on failed its check). Each problem and each breach has a `code`
596
- in plain words and a `message`. The codes are listed in
597
- `docs/threat-model.md`, section 8.
598
-
599
- The `content` of an entry that has problems, or whose `verified` is
600
- `none`, is unverified. It is there for whoever must find out what went
601
- wrong. Never show it as fact: the checking page and the command-line
602
- checker do not.
603
-
604
- Options: `issuerKeys` (the thumbprints of issuer keys you trust),
605
- `sealKeys` (the fingerprints of the key sets of the recorders you
606
- expect), `stampServices` (the fingerprints of the certificates of the
607
- time-stamp services you trust), `blocks` (the fingerprints of the
608
- blocks of a public blockchain you trust), `vouchers` (the fingerprints of the key
609
- sets of the organisations whose vouching you trust), `disclosures` (the
610
- disclosures of covered fields that you were handed), `cancellations`
611
- (the person's own copies of cancellations), `expectedRoot` and
612
- `expectedSize` (a top fingerprint and a number of entries you already
613
- hold a copy of). Give
614
- them whenever you have them.
615
-
616
- The checker reads a whole book into memory. A book of several hundred
617
- megabytes may be too large for a browser or for Node.js to hold as one
618
- text.
619
-
620
- The command-line checker ends with code 0 (intact, nothing outside the
621
- slip, and checked against keys you named: the passkey you trust and,
622
- where the record has a seal, the recorder you expect), 1 (no problem
623
- found, but the agent is outside its slip, not every signature could be
624
- checked, or you did not name whom you trust) or 2 (a problem was found,
625
- or what you gave on the command line could not be used). A value that
626
- is not a fingerprint is refused, never set aside. The checking page
627
- gives its green answer on the same terms.
628
-
629
- Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
630
- `assembleApproval` (the person), `writeStub` (the agent), `countersign`,
631
- `prepareCancellation` and `assembleCancellation` (the person),
632
- `writeRefusal` and `writeTerms` (the service), `writeVouching` and
633
- `writeWithdrawal` (an organisation), `writePass` (an agent, to a helper
634
- agent), `writeSeal` (whoever keeps
635
- the book; it needs Node.js), `writeBook` and `makeShow`, all from
636
- `provared`. The library asks no time-stamp service itself: you send
637
- the seal's fingerprint to the service you choose and put its answer
638
- beside the seal. In a browser, `createPasskey`, `signSlipWithPasskey`,
639
- `approveWithPasskey` and `cancelWithPasskey` from
640
- `provared/passkey-browser`. For a slip with covered fields in a browser:
641
- `prepareSlip` with `cover`, then `signChallengeWithPasskey` with what it
642
- prepared, then `assembleSlip`; keep the disclosures it prepared.
81
+ | **The permission (a Slip)** | A person signs it with a passkey: which agent, which actions, what limits and conditions, with whom, from when until when, and why |
82
+ | **The receipt (a Stub)** | The agent signs one for each action, pointing back to its slip; the other side countersigns it where it can, and the person approves it where the slip asks |
83
+ | **The sealed book (a Seal)** | The receipts chained, gathered under one fingerprint, signed by whoever keeps the book and time-stamped by an outside service |
84
+ | **One page with proof (a Show)** | One entry handed to a checker with proof that it is in the book, and when the book was sealed, with the names it does not need kept covered |
85
+
86
+ The checker answers two questions and keeps them apart: **is the record
87
+ intact**, and **did the agent stay within its slip**. Going over a limit
88
+ is not a fault in the record. It is what a sound record shows.
89
+
90
+ ## What the author found nowhere else
91
+
92
+ Signed, chained and time-stamped receipts for an agent's actions exist
93
+ in other open projects too, some with quantum-safe signatures. As far as
94
+ the author knows, in October 2026, three things are only here: **limits
95
+ a machine can check** in a permission the person signs, with the
96
+ checker's answer "did the agent stay within it"; **passing a permission
97
+ on** to a helper agent so that it can narrow and never widen; and
98
+ **cancelling** with the agent's signed acknowledgement, so that a record
99
+ shows when the agent's side was told. If you know of another project
100
+ that does any of these, open an issue: the list is kept honest.
643
101
 
644
102
  ## What a record does not prove
645
103
 
646
- - It proves what was recorded, not what was left out. An agent that acts
647
- and writes no stub leaves no trace.
648
- - A one-sided stub (one with no countersignature) proves what the agent's
649
- side said, not what the other side did.
650
- - A refusal proves what the service's side said, not what the agent did.
651
- - A rule of conduct in a slip is the person's signed instruction. No
652
- check can show that the agent kept it.
653
- - An approval proves that the passkey approved that one action, not that
654
- the person read what they approved.
655
- - It does not show that an action was wise, lawful or wanted.
656
- - A badly written slip is faithfully recorded as a badly written slip.
657
- - It does not prove who was holding the device when the passkey signed.
658
- - A name in a record is a label. Only keys are checked. A vouching
659
- record shows which organisation stood behind a name, not how well it
660
- checked.
661
-
662
- And about time:
663
-
664
- - only an outside time-stamp you trust, from a service or a block you
665
- named, proves when entries existed. The times inside records are the signer's own word,
666
- so a limit within a period rests on the agent's word;
667
- - after the last seal, the latest stubs under a slip or a pass, or a
668
- slip or a pass with all its stubs, can be removed unnoticed, which can hide a stub that shows
669
- the agent outside its slip;
670
- - the last seal, with everything after it, can itself be removed. Only a
671
- copy of it, or of its top fingerprint and number of entries, held by
672
- someone else shows that;
673
- - time-stamp services sign with methods of today's kind, which are not
674
- quantum-safe.
104
+ It shows what was recorded, not what was left out: an agent that acts
105
+ and writes no stub leaves no trace. A one-sided stub shows what the
106
+ agent's side said. It does not show that an action was wise, lawful or
107
+ wanted, nor who was holding the device when the passkey signed. A name
108
+ in a record is a label; only keys are checked. The full list, with what
109
+ each part protects against, is in
110
+ [docs/threat-model.md](docs/threat-model.md).
111
+
112
+ ## Where everything else is
113
+
114
+ - **[The guide](docs/guide.md)**: the whole library, in one document.
115
+ Installing and importing; the command-line checker; what a slip can
116
+ say; seals, time-stamps and the chain of block headers; cancelling;
117
+ who stands behind a name; covered fields; passing a slip on; the check
118
+ before acting; the stub writer beside an agent and the connector for
119
+ its tools; what the checker returns; what it never does; what is not
120
+ built yet.
121
+ - **[The format, described exactly](spec/provared-format.md)**, and
122
+ the shared test files in [test-vectors/](test-vectors/) that any
123
+ implementation can be held to.
124
+ - **[The Python version](python/README.md)**, which gives the same
125
+ answers, with examples for three agent frameworks and packages for
126
+ two of them.
127
+ - **[The pages at prova.red](https://prova.red/)**: the checker's answer
128
+ for the sample record, a worked example for developers, and the same
129
+ in plain words for a reader who does not write code.
130
+ - **[The checking page](https://prova.red/check/)**, which checks a
131
+ record in a browser and loads nothing from anywhere; also built as
132
+ one file, attached to each release.
133
+ - **The core of the format as an individual Internet-Draft** at the
134
+ IETF:
135
+ [draft-izmaylov-agent-permission-receipts](https://datatracker.ietf.org/doc/draft-izmaylov-agent-permission-receipts/).
136
+ An Internet-Draft is a working document, not a standard.
137
+ - **[CHANGELOG.md](CHANGELOG.md)**, [SECURITY.md](SECURITY.md) and
138
+ [CONTRIBUTING.md](CONTRIBUTING.md).
675
139
 
676
- ## What it never does
677
-
678
- It never holds money, never starts a payment and issues no token. It
679
- reports nothing back from the machine it runs on. The checker makes no
680
- network request. Only `provared-headers`, which you run yourself,
681
- connects to anything: to the Bitcoin nodes you name, for block headers.
682
-
683
- ## Not built yet
140
+ ## Status
684
141
 
685
- A public place where seals are kept; covering fields other than a slip's
686
- names and purpose; connectors for particular agent software (the
687
- general one, `recordTools`, is built); a compact binary form; the
688
- recorder service. The checking page cannot yet take a chain of block
689
- headers: use the command-line checker.
142
+ **Version 0.3.0, a draft.** Published as it is, by one author in their
143
+ own time, built with AI tools since 3 October 2026; fixes as time
144
+ allows. The code has been through 25 rounds of review by AI reviewers,
145
+ each a fresh session given only the code and the documents; every fault
146
+ was fixed and kept as a test. No person other than the author has
147
+ reviewed it yet. That is an invitation. The format may still change.
148
+ Every published version stays on npm and the Python Package Index, so a
149
+ record made with one version can always be checked by that version.
150
+
151
+ Questions, faults and suggestions: hello@prova.red, or
152
+ [issues on GitHub](https://github.com/provared/provared/issues).
690
153
 
691
154
  ## Licences
692
155