provared 0.2.0 → 0.4.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/CHANGELOG.md +324 -0
- package/README.md +134 -653
- package/bin/provared-check.mjs +39 -2
- package/docs/guide.md +745 -0
- package/docs/threat-model.md +16 -7
- package/package.json +25 -2
- package/page/index.html +1 -1
- package/page/style.css +3 -0
- package/spec/provared-format.md +30 -7
- package/src/dev.d.ts +54 -0
- package/src/dev.js +123 -0
- package/src/guard.js +73 -5
- package/src/recorder.js +12 -2
- package/src/signatures.js +7 -0
- package/src/types.d.ts +2 -0
package/README.md
CHANGED
|
@@ -1,692 +1,173 @@
|
|
|
1
|
-
# Provared
|
|
1
|
+
# <img src="https://prova.red/favicon.svg" alt="" width="36"> Provared
|
|
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
|
-
```
|
|
53
|
-
npm install provared
|
|
54
|
-
```
|
|
55
|
-
|
|
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.
|
|
59
|
-
|
|
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
|
-
```
|
|
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>
|
|
70
|
-
```
|
|
71
|
-
|
|
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
|
-
```
|
|
86
|
-
|
|
87
|
-
**Check the sample record from the command line.**
|
|
88
|
-
|
|
89
|
-
```
|
|
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
12
|
```
|
|
122
|
-
npm
|
|
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
|
|
123
16
|
```
|
|
124
17
|
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
18
|
+
The sample is an invented office agent that goes over its limit at its
|
|
19
|
+
fifth order. The checker's answer ends:
|
|
191
20
|
|
|
192
21
|
```
|
|
193
|
-
|
|
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
|
|
194
28
|
```
|
|
195
29
|
|
|
196
|
-
|
|
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.
|
|
30
|
+
## A first record of your own
|
|
205
31
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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).
|
|
209
37
|
|
|
210
38
|
```
|
|
211
|
-
|
|
212
|
-
|
|
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 } });
|
|
39
|
+
npm install provared
|
|
40
|
+
node first-record.mjs
|
|
41
|
+
npx provared-check book.jsonl --issuer <the thumbprint it prints>
|
|
308
42
|
```
|
|
309
43
|
|
|
310
|
-
|
|
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.
|
|
44
|
+
`first-record.mjs` ([examples/first-record.mjs](examples/first-record.mjs)):
|
|
336
45
|
|
|
337
46
|
```js
|
|
338
|
-
import {
|
|
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 });
|
|
47
|
+
import { writeFileSync } from 'node:fs';
|
|
48
|
+
import { developmentRecorder } from 'provared/dev';
|
|
375
49
|
|
|
376
|
-
const
|
|
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
|
-
);
|
|
50
|
+
const SEND = 'provared.message.send';
|
|
381
51
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
473
|
-
|
|
474
|
-
```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) },
|
|
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.',
|
|
487
57
|
});
|
|
488
58
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
515
|
-
|
|
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.
|
|
529
|
-
|
|
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'],
|
|
537
|
-
});
|
|
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
|
+
}
|
|
538
65
|
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
66
|
+
writeFileSync('book.jsonl', recorder.book());
|
|
67
|
+
console.log(`npx provared-check book.jsonl --issuer ${issuerKeys[0]}`);
|
|
544
68
|
```
|
|
545
69
|
|
|
546
|
-
|
|
547
|
-
|
|
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)):
|
|
548
76
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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.
|
|
77
|
+
```py
|
|
78
|
+
from langchain.agents import create_agent
|
|
79
|
+
from provared_langchain import ProvaredMiddleware
|
|
555
80
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
81
|
+
agent = create_agent(model, tools=[order_supplies, send_message],
|
|
82
|
+
middleware=[..., ProvaredMiddleware(writer, {
|
|
83
|
+
'order_supplies': {'action': 'supplies.order', 'with': 'supplier',
|
|
84
|
+
'amount': lambda a: {'unit': 'GBP', 'value': a['total_gbp']}},
|
|
85
|
+
'send_message': 'provared.message.send',
|
|
86
|
+
})]) # last among the middleware that wrap tool calls
|
|
562
87
|
```
|
|
563
88
|
|
|
564
|
-
|
|
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:
|
|
569
|
-
|
|
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
|
|
89
|
+
## The four parts
|
|
576
90
|
|
|
577
|
-
|
|
|
91
|
+
| Part | What it is |
|
|
578
92
|
|---|---|
|
|
579
|
-
|
|
|
580
|
-
|
|
|
581
|
-
|
|
|
582
|
-
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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.
|
|
93
|
+
| **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 |
|
|
94
|
+
| **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 |
|
|
95
|
+
| **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 |
|
|
96
|
+
| **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 |
|
|
97
|
+
|
|
98
|
+
The checker answers two questions and keeps them apart: **is the record
|
|
99
|
+
intact**, and **did the agent stay within its slip**. Going over a limit
|
|
100
|
+
is not a fault in the record. It is what a sound record shows.
|
|
101
|
+
|
|
102
|
+
## What the author found nowhere else
|
|
103
|
+
|
|
104
|
+
Signed, chained and time-stamped receipts for an agent's actions exist
|
|
105
|
+
in other open projects too, some with quantum-safe signatures. As far as
|
|
106
|
+
the author knows, in October 2026, three things are only here: **limits
|
|
107
|
+
a machine can check** in a permission the person signs, with the
|
|
108
|
+
checker's answer "did the agent stay within it"; **passing a permission
|
|
109
|
+
on** to a helper agent so that it can narrow and never widen; and
|
|
110
|
+
**cancelling** with the agent's signed acknowledgement, so that a record
|
|
111
|
+
shows when the agent's side was told. If you know of another project
|
|
112
|
+
that does any of these, open an issue: the list is kept honest.
|
|
643
113
|
|
|
644
114
|
## What a record does not prove
|
|
645
115
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
116
|
+
It shows what was recorded, not what was left out: an agent that acts
|
|
117
|
+
and writes no stub leaves no trace. A one-sided stub shows what the
|
|
118
|
+
agent's side said. It does not show that an action was wise, lawful or
|
|
119
|
+
wanted, nor who was holding the device when the passkey signed. A name
|
|
120
|
+
in a record is a label; only keys are checked. The full list, with what
|
|
121
|
+
each part protects against, is in
|
|
122
|
+
[docs/threat-model.md](docs/threat-model.md).
|
|
123
|
+
|
|
124
|
+
## Where everything else is
|
|
125
|
+
|
|
126
|
+
- **[The guide](docs/guide.md)**: the whole library, in one document.
|
|
127
|
+
Installing and importing; the command-line checker; what a slip can
|
|
128
|
+
say; seals, time-stamps and the chain of block headers; cancelling;
|
|
129
|
+
who stands behind a name; covered fields; passing a slip on; the check
|
|
130
|
+
before acting; the stub writer beside an agent and the connector for
|
|
131
|
+
its tools; what the checker returns; what it never does; what is not
|
|
132
|
+
built yet.
|
|
133
|
+
- **[The format, described exactly](spec/provared-format.md)**, and
|
|
134
|
+
the shared test files in [test-vectors/](test-vectors/) that any
|
|
135
|
+
implementation can be held to.
|
|
136
|
+
- **[The Python version](python/README.md)**, which gives the same
|
|
137
|
+
answers, with examples for three agent frameworks and packages for
|
|
138
|
+
two of them.
|
|
139
|
+
- **[The pages at prova.red](https://prova.red/)**: the checker's answer
|
|
140
|
+
for the sample record, a worked example for developers, and the same
|
|
141
|
+
in plain words for a reader who does not write code.
|
|
142
|
+
- **[The checking page](https://prova.red/check/)**, which checks a
|
|
143
|
+
record in a browser and loads nothing from anywhere; also built as
|
|
144
|
+
one file, attached to each release. It uses only the signing methods
|
|
145
|
+
the browser has built in. Tried on the author's computer on
|
|
146
|
+
13 October 2026: Chrome 155, Edge 155 and Brave 155 each check the
|
|
147
|
+
quantum-safe signatures on receipts (ML-DSA). None of the three has the
|
|
148
|
+
third method a seal is signed with (SLH-DSA), so in a browser a book
|
|
149
|
+
with a seal is never a pass. The command-line checker checks all three.
|
|
150
|
+
- **The core of the format as an individual Internet-Draft** at the
|
|
151
|
+
IETF:
|
|
152
|
+
[draft-izmaylov-agent-permission-receipts](https://datatracker.ietf.org/doc/draft-izmaylov-agent-permission-receipts/).
|
|
153
|
+
An Internet-Draft is a working document, not a standard.
|
|
154
|
+
- **[CHANGELOG.md](CHANGELOG.md)**, [SECURITY.md](SECURITY.md) and
|
|
155
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
156
|
+
|
|
157
|
+
## Status
|
|
158
|
+
|
|
159
|
+
**Version 0.4.0, a draft.** Published as it is, by one author in their
|
|
160
|
+
own time, built with AI tools since 3 October 2026; fixes as time
|
|
161
|
+
allows. The code has been through 40 rounds of review by AI reviewers,
|
|
162
|
+
each reviewer a fresh session given the code, the documents and a brief
|
|
163
|
+
of what to look at; every fault was fixed, or stated as a limit, and
|
|
164
|
+
kept as a test. No person other than the author has reviewed it yet.
|
|
165
|
+
That is an invitation. The format may still change.
|
|
166
|
+
Every published version stays on npm and the Python Package Index, so a
|
|
167
|
+
record made with one version can always be checked by that version.
|
|
168
|
+
|
|
169
|
+
Questions, faults and suggestions: hello@prova.red, or
|
|
170
|
+
[issues on GitHub](https://github.com/provared/provared/issues).
|
|
690
171
|
|
|
691
172
|
## Licences
|
|
692
173
|
|