provared 0.3.0 → 0.5.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 CHANGED
@@ -1,5 +1,345 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (17 October 2026)
4
+
5
+ - **The README's count of review rounds: 50.** The 40 recorded by
6
+ version 0.4.0, and 10 since: five of the Agno package, three of the
7
+ signer interface and two of the stub bound to the platform's own
8
+ record, each listed below.
9
+ - **`provared-agno` 0.1.0, an Agno tool hook**
10
+ (`python/integrations/provared-agno/`): every tool call of an Agno
11
+ agent is checked against the slip before it runs, and a stub is
12
+ written once it has run. A call the slip does not allow is not run, and
13
+ the model is told why. Agno's own steering tools cannot carry an action
14
+ past the record: a hand-over, or a task in a team's task mode, goes
15
+ only to a member with the same writer's hooks, read from Agno's own
16
+ list of tasks, not from the session's copy that the model can rewrite.
17
+ A stub is written only once its tool has completed, even where a
18
+ helper agent runs inside the tool, on the same event loop or on one of
19
+ its own; a tool that never completes leaves none, and a cancelled tool
20
+ that completes all the same is recorded. Checked by an independent reviewer
21
+ over five rounds (faults found in the first four, the fifth clean);
22
+ each fault is a test, 68 in all. Not on the Python Package Index yet:
23
+ the README gives the command to install it from this repository.
24
+ - **Signers: keys held elsewhere, in both languages.** Wherever a writer
25
+ takes `privateKeys` (`private_keys` in Python), each item may be a key,
26
+ as before, or a signer: an object with `key`, the public key, and
27
+ `sign(data)`, which gives the signature bytes. So a key may live in a
28
+ key service or a hardware module. `signerFromKey` (`signer_from_key`)
29
+ wraps a key held in memory as a signer. A record made through a signer
30
+ is the same record: the same content, labels and Ed25519 signature. A
31
+ signer is not taken on trust: before any signer is asked to sign, its
32
+ key must be plain data, of the method of its place, of a method the
33
+ device can check, and, where the writer knows it, the key the record
34
+ must be signed with (`bad-key`); every signature it gives is checked
35
+ against its key before it is used, and one of the wrong length, one
36
+ that does not check, something other than bytes or an error leaves no
37
+ record (`signature-invalid`), with the method and the key named. The
38
+ stub writer gives a signer 30 seconds for each signature (`signWithin`,
39
+ `sign_within`); after that nothing is written and the writer goes on.
40
+ In Python no seal is written, with keys or signers, as before. Every
41
+ private key that made a correct record before still does, in the same
42
+ way: in JavaScript a private Web Crypto key of the place's method for
43
+ Ed25519 and ML-DSA-87, and for a seal's SLH-DSA-SHA2-256s a Node.js
44
+ KeyObject or what Node.js's own signing call read before (PEM text,
45
+ DER bytes with their format, a JSON Web Key with its format); in Python
46
+ a private key of `cryptography` of the place's method. Refused now
47
+ (`bad-field`), though accepted before: in JavaScript, a private key of
48
+ another kind in a seal's SLH-DSA place, and in Python a private key of
49
+ another method or kind (such as Ed448 in the Ed25519 place); each made
50
+ a record that failed its own check. In Python, an object with only
51
+ `sign` now needs `key` as well: it is a signer. Anything else that is
52
+ neither a key nor a signer (a public key, a key of another method in
53
+ JavaScript's first two places, a value that only looks like a key) is
54
+ refused with `bad-field`, where before it failed with the platform's
55
+ own error. No record and no rule of the format changes. `signWithin`
56
+ must be 1 to 2,147,483,647 milliseconds (`bad-field`). Checked by an
57
+ independent reviewer over three rounds, the third clean; each fault is
58
+ fixed and is now a test.
59
+ Tests: `test/signers.test.mjs`, `test/signers-unavailable.test.mjs` and
60
+ `python/tests/test_signers.py`.
61
+ - **A stub bound to the platform's own record of the call, in both
62
+ languages** (format description, section 27). Document names that
63
+ begin `provared.` are now reserved: seven are listed
64
+ (`provared.arguments`, `provared.http-request`,
65
+ `provared.http-response`, `provared.trace-id`, `provared.request-id`,
66
+ `provared.event-id`, `provared.error`), each with the exact text its
67
+ fingerprint is made from, so that someone who holds another system's
68
+ own log can match a stub to it. Only the three identifiers
69
+ (`trace-id`, `request-id`, `event-id`) pick out one call; the others
70
+ show only that the content matches, and the request and answer
71
+ fingerprints cover no header. Any other name that begins `provared.`
72
+ is refused (`bad-field`) in a stub, an approval and the check before
73
+ acting; before, it was a label. So is any of the seven but the three
74
+ identifiers named twice in one record. **A book written before this
75
+ change that holds a stub, or an approval, with such a name now fails
76
+ its check, and the check before acting then refuses every action
77
+ under it** (and a stub writer will not open on it). A checker of
78
+ version 0.4.0 or earlier reads the reserved names as labels.
79
+ `DOCUMENT_NAMES`,
80
+ `httpRequestFingerprint`, `httpResponseFingerprint` and
81
+ `identifierFingerprint` (`http_request_fingerprint` and so on in
82
+ Python) make the fingerprints; an HTTP request or answer is written as
83
+ the signature base of RFC 9421, with the Content-Digest of RFC 9530
84
+ over the body after any content coding, and its address is taken
85
+ exactly as sent; a body of null (None) is no body. The stub writer's `act`
86
+ takes `detailsAfter` (`details_after` in Python): up to four further
87
+ documents, from what the action gave back, such as the identifier a
88
+ platform gave the call. The stub is still checked with the whole book
89
+ before the action; after it, the stub is signed again with them in the
90
+ same call, and keeps the time the action began. If they cannot go in,
91
+ the stub is written without them and `detailsAfter` in the answer says
92
+ why: an action that was taken always gets its stub (where the second
93
+ signing fails, it says so: `details-not-added`). It cannot be used
94
+ with an approval. With it, the request may name at most 28 documents
95
+ (`bad-field`), and an action whose stub would leave no room for four
96
+ more is not taken (`too-large`); `detailsAfter` is given as long as the
97
+ other side is given to countersign. `recordTools` and `record_function` still name the
98
+ arguments `arguments` by default. Shared test files: `stub-contents.json`
99
+ gains cases (made again; its earlier cases are unchanged), and
100
+ `http-fingerprints.json` and `recorder-details-after.json` are new.
101
+ Checked by an independent reviewer in two rounds, the second clean.
102
+ Tests: `test/documents.test.mjs` and `python/tests/test_documents.py`.
103
+
104
+ ## 0.4.0 (14 October 2026)
105
+
106
+ - **The command-line checker counts the keys it was given.** With two
107
+ passkeys named, its summary said "yes: the passkey you trust"; it now
108
+ says "the passkeys you trust", and "the recorders you expect" where
109
+ more than one recorder is named; the same key named twice counts once.
110
+ Only the words change; tests hold them. The guide gives the fingerprint
111
+ of the checking page built as one file for this version, which changed
112
+ because the checker it holds, and the page itself, changed.
113
+ - **The README's count of review rounds is brought up to date: 40.** The
114
+ 25 recorded by version 0.3.0, and 15 since: three of the library's
115
+ changes of 11 to 13 October (the seal rule, then a narrow review of four
116
+ commits and a check of its fixes), three of the signing page, six of the
117
+ recorder beside Claude Code, two of the record page's book in a release,
118
+ and one of the asynchronous reads below. Each reviewer is a fresh
119
+ session given the code, the documents and a brief of what to look at;
120
+ the README now says so, and that a fault may be stated as a limit rather
121
+ than fixed.
122
+ - **The two framework packages require version 0.4.0**, so that an
123
+ agent behind either goes on after its book is sealed (below).
124
+ `provared-langchain` becomes 0.1.1, which also carries the keywords,
125
+ the homepage and the mark added on 10 and 11 October.
126
+ `pydantic-ai-provared` 0.1.0 is its first version; its README already
127
+ gave the command to install it. Both are published separately, on the
128
+ Python Package Index.
129
+ - **The LangChain and Pydantic AI packages read the stub writer safely
130
+ from asynchronous code**: `await middleware.snapshot()` and
131
+ `await capability.snapshot()` give the book and the cancellations the
132
+ writer holds, waiting in a thread of their own. The READMEs said to
133
+ read it with `asyncio.to_thread(writer.snapshot)`; but in the event
134
+ loop's shared threads such a read waits for the action in progress,
135
+ and enough of them at once leave no shared thread for a tool that
136
+ needs one, so the tool and the reads wait for ever. Found while
137
+ building a package for another framework; checked by an independent
138
+ reviewer, and a test in each package fails without it.
139
+ - **A proposed action dated by text gets a plain refusal.** The check
140
+ before acting took a time given as text, or a number or a Date that
141
+ gives no time, to the catch-all "the checker met something it did not
142
+ expect" (`check-failed`). It now answers `bad-field`: "when: must be a
143
+ number of milliseconds, or a Date" (the stub writer's own words), or
144
+ "when: does not give a time" (new words, for a number such as Infinity,
145
+ a Date that is not a time, or a Date wrapped in a Proxy). In Python a
146
+ datetime counts as dated only where its time zone gives an offset, as
147
+ Python's own rule for an "aware" time says. Nothing that was allowed is
148
+ refused, and nothing that was refused is allowed. The stub writer
149
+ answers in the same words: its "before" and "act" took a Date wrapped
150
+ in a Proxy to a plain TypeError, and now answer "when: does not give a
151
+ time", as the check before acting does; its "record", which does not
152
+ ask first, took any time that gives none to a plain "Invalid time
153
+ value" error, and now refuses it with `bad-field` in those words. In
154
+ both languages; the shared test file for the check before acting gains
155
+ two cases, and in the stub writer's scenarios the one step that hands in
156
+ a time of Infinity changes its words, and the one step that records an
157
+ action at a time outside the range of times changes its answer. Tests
158
+ in both languages hold a Date in a Proxy and, in Python, a time zone
159
+ that gives no offset.
160
+ - **A record's signature of the wrong length is invalid on every
161
+ device.** An Ed25519 signature is 64 bytes, an ML-DSA-87 signature 4,627
162
+ and an SLH-DSA-SHA2-256s signature 29,792 (RFC 8032, FIPS 204, FIPS 205).
163
+ One of another length can never check, and a device that has the method
164
+ already said "invalid"; a device without it said "not checked on this
165
+ device". Now every device says "invalid", for every signature of a
166
+ record, the passkey's Ed25519 signature on a slip among them. So, on a
167
+ device without SLH-DSA, a seal whose third signature is cut short or
168
+ empty is a problem, not something the check before acting goes on
169
+ without. The signature inside a time-stamp from a service is not
170
+ covered: on a device without its method a time-stamp is not checked,
171
+ whatever its length, and is not counted. A third signature of the right
172
+ length that would not check is still not seen on a device without
173
+ SLH-DSA. The format description states the rule in section 3.9 and
174
+ what it means for a seal in section 20; the list of threats says that a
175
+ third signature of the right length is not seen there. In both
176
+ languages; the shared test files for a whole book and for the check
177
+ before acting gain a case each, and tests in both languages hold each
178
+ method's length, shorter and longer, with the method and without it.
179
+ - **The check before acting goes on without a seal's third signature, and
180
+ says so** (decision F-106; this changes choice 66 of the draft). Found on
181
+ 11 October 2026: a device that cannot check SLH-DSA-SHA2-256s, which is
182
+ every Python stub writer (and so the LangChain and Pydantic AI packages)
183
+ and every browser, was refused every action as soon as its book held a
184
+ seal, because the check allowed nothing unless every signature in the
185
+ record so far had been checked on that device. Now, where the only
186
+ signatures not checked are the third ones of seals whose first two were
187
+ checked there, and nothing else was left unchecked, the check goes on and
188
+ its answer carries `notChecked: ['SLH-DSA-SHA2-256s']`. Any other
189
+ signature left unchecked still refuses everything, and the full checker's
190
+ answers are unchanged. In both languages: the shared test file for the
191
+ check before acting gains seven cases with a seal, besides the one with
192
+ a third signature cut short counted above (found missing by two
193
+ independent reviews, which showed that weaker rules passed every test:
194
+ the last case, a permission and a seal alone on a device without
195
+ ML-DSA-87, holds the rule that the seal's ML-DSA-87 signature must have
196
+ been checked), and the shared test file for the stub writer's scenarios
197
+ is remade. Tests in both languages hold the same rule.
198
+ - **The signing page names the agent's keys by the fingerprint of their
199
+ key set** before the passkey is asked, worked out by the library's own
200
+ `keySetFingerprint` (as a recorder's key set is named), and asks the
201
+ person to compare it with the one the agent's software printed when it
202
+ made its keys: a link, or a permission pasted from elsewhere, could
203
+ carry someone else's keys under a familiar name. Keys that are not a
204
+ key set are refused there, in the library's own words. A test holds the
205
+ page's words against the library's fingerprint.
206
+ - **The signing page fills itself from a link, and shows everything a
207
+ link could slip in before the passkey is asked.** A link to
208
+ prova.red/sign/ may hold a starting permission after "#"; that part
209
+ stays in the browser and is never sent to a server. A link that cannot
210
+ be read leaves the page's own starting point and says why; a second
211
+ link opened in the same tab fills the page again. Because a link can
212
+ name anything, the page now shows, besides the agent's key-set
213
+ fingerprint (above): each service's keys by the fingerprint of their key
214
+ set, as a checker names them (keys that are not a key set are refused,
215
+ in the library's own words); and the agent's software statement, marked
216
+ as the person's statement and not proof of what runs. Shown inside
217
+ another page, it offers nothing to sign or cancel, since another site
218
+ could frame it filled from a link of its choosing. It now has a content
219
+ security policy that lets it load only its own scripts and style sheet
220
+ and send nothing anywhere; the message of the commit that added the
221
+ link said its policy was unchanged, but the page had none. Tests hold
222
+ each point. Checked by an independent reviewer over three rounds.
223
+ - **The recorder beside Claude Code is a package of its own,
224
+ `provared-claude-code` 0.1.0** (published separately, on npm), in
225
+ `integrations/claude-code/`, in place of the recorder of 10 October.
226
+ `init` makes the keys, writes the hooks and prints a link to the signing
227
+ page filled in with a starting permission; `use` puts the signed
228
+ permission in place; `renew` takes a new permission while the recorder
229
+ runs, with no moment in which a call finds it gone. A permission from a
230
+ passkey the recorder has not trusted before is taken only with
231
+ `--new-passkey`, and the passkeys it trusts are kept in
232
+ `trusted-passkeys.json`. The hooks `init` writes match every tool,
233
+ record calls that failed, and refuse a call where the hook itself fails
234
+ (`"onFailure": "block"`). A command is named by reading its line three
235
+ ways (as a shell splits it, from every known program's name, and with
236
+ the first version's patterns), and the strictest name counts; a line
237
+ handed to a shell, to `eval`, `iex` or `Invoke-Expression`, and the
238
+ command `find -exec` runs are read the same way. A call allowed under
239
+ one permission and taken while a renewal switched to the next is
240
+ recorded under the one it was allowed under, and the stub of a call that
241
+ was taken is always written. `keys` and `init` print the fingerprint of
242
+ the agent's key set, which the signing page asks the person to compare;
243
+ `seal` shows the recorder's error. Checked by an independent reviewer in
244
+ six rounds; each fault found is a test (57 in all).
245
+ - **The record page's book and the checker's answer are kept in a release
246
+ of the repository, not in the repository itself**: they grow by
247
+ megabytes a day, and GitHub refuses a file over 100 MB.
248
+ `tools/make-record-page.mjs` writes their SHA-256 fingerprints beside
249
+ the page (`site/records/files.sha256`); the site's publishing run
250
+ fetches both from the release "record" and publishes nothing unless each
251
+ matches, and every file the page links is there. Readers see no change.
252
+ The tool now reads the growing book once, into the page's own folder,
253
+ leaving out a last line still being written, so that the page, the
254
+ checker's answer and the fingerprints describe one book (the page of
255
+ 14 October said 3,771 receipts above a summary of 3,770 stubs). It
256
+ counts the passkeys that signed the book's permissions from the book,
257
+ and refuses a passkey named to it that signed none. Checked by an
258
+ independent reviewer in two rounds.
259
+ - **What browsers check, tried again on 13 October 2026.** Chrome 155,
260
+ Edge 155 and Brave 155 each offer Ed25519 and ML-DSA (all three
261
+ strengths) to a page and check real signatures made with them; none of
262
+ them offers SLH-DSA. The README, the guide, the list of threats, the
263
+ home page, the worked example and the tool that writes the record page
264
+ now say so; they said, from a try on 12 October, that Chrome 154 did
265
+ not check ML-DSA. No code changes.
266
+ - **The recorder beside Claude Code appends to the book, and no longer
267
+ loses a receipt while another program reads the file.** It wrote the
268
+ whole book to a temporary file and renamed it over the old one; on
269
+ Windows that is refused while anyone holds the file open (an uploader,
270
+ a virus scanner), and the hook then reported "the action was taken and
271
+ is not in the record". Found on 11 October 2026, when a program that
272
+ uploads the book was reading it every two seconds: 7 of one helper's 23
273
+ tool calls were taken and not recorded. A book only grows, so the
274
+ recorder now appends the new lines; where the file does not match the
275
+ book it is replaced, with retries for up to two seconds. A test holds
276
+ the file open while the recorder saves.
277
+
278
+ - 2026-10-11: **one mark everywhere**: a permission slip with a
279
+ signature, white on red (#8f000f), as `site/favicon.svg`, in the
280
+ social preview `site/preview.png` beside the word, as inline SVG in the
281
+ header of every page at prova.red, of the checking page and of the
282
+ signing page (no page makes a request for it), and at the top of the
283
+ README and of the three Python packages' READMEs, which the registries
284
+ show with the next published version. The four colours of the parts stay as they are inside the
285
+ pages. The style sheet gains one rule for the mark and a new version
286
+ number. No code changes.
287
+ - 2026-10-10: **a real record on prova.red**, `tools/make-record-page.mjs`:
288
+ writes `site/records/` from the book that the recorder beside Claude
289
+ Code keeps. It copies the book and its slip beside the page, runs the
290
+ command-line checker with the passkey and the recorder the reader is
291
+ told to trust, keeps the whole answer as a text file, and writes the
292
+ page from what the checker answered: the slip in the checker's own
293
+ words, the counts by day, by kind of action and by tool, the seals,
294
+ any entry outside the permission, the summary exactly as printed, and
295
+ what the check does not show. The page says that a refused call
296
+ leaves no entry and that every receipt is one-sided. It is run again
297
+ each day while the permission lasts; the home page links to it. A
298
+ test runs the tool on the sample record into a folder of its own.
299
+ - 2026-10-10: **the pages at prova.red, the README and the framework
300
+ packages, after the adoption review**: the line under each page's
301
+ title names its reader and then the slogan; "How it is built" on the
302
+ home page is two sentences; the developer page says "the person"
303
+ throughout, names the nearest other projects beside what the
304
+ author found nowhere else, and gives the author's own reason for
305
+ building it; the plain-words page names Article 12 and
306
+ Article 26 of the AI Act, no longer repeats its opening, and asks a
307
+ reader who breaks it to open an issue; every page ends with two
308
+ lines about the author; the README shows the LangChain example; the
309
+ two framework packages gain keywords and a homepage. No code changes.
310
+ - 2026-10-10: **a signing page**, `page/sign.html`, served at
311
+ prova.red/sign/: a person writes a permission as JSON (the agent's
312
+ name and two public keys, the actions, limits, conditions, stated
313
+ rules, with whom, from when until when, and why), sees it in words,
314
+ signs it with a passkey made for the website, and saves the signed
315
+ file; the page shows the passkey's thumbprint and the permission's
316
+ fingerprint. It can also sign a cancellation of a permission signed
317
+ with the same passkey. It uses the library's browser part and
318
+ `render.js`, loads nothing from anywhere else and sends nothing. The
319
+ workflow serves it beside the checking page.
320
+ - 2026-10-10: **the recorder beside Claude Code**, `integrations/claude-code/`:
321
+ a hook that Claude Code runs before and after each tool call, and a
322
+ small service on 127.0.0.1 that holds the stub writer open on one book
323
+ under a slip the person signed. Before a call, the check before acting
324
+ refuses a call outside the slip and Claude Code is told why; after it,
325
+ a stub is written naming the tool and the SHA-256 fingerprint of its
326
+ arguments; the service seals the book on request with the recorder's
327
+ three keys. Tool calls are named from the shared list by the hook,
328
+ not by the agent (a command that removes something is
329
+ `provared.data.delete`; one that publishes is `provared.code.release`);
330
+ tools that only talk to the person are not recorded. The agent's two
331
+ keys and the recorder's three are kept as JSON Web Keys in files the
332
+ person keeps. If the service is not running, a call is refused: no
333
+ record, no action. The folder names a product, as the framework
334
+ packages do; the library names none, and the npm package does not
335
+ hold it. Five tests run the hook as Claude Code runs it.
336
+ - **The tests clean up after themselves.** Nine test files made a
337
+ temporary folder for each run and left it behind; on 13 October 2026
338
+ the computer's temporary folder held about 2,800 of them. A small
339
+ helper, `test/helpers/temp.mjs`, now makes each folder and removes it
340
+ when that test file's tests end. Run with an empty temporary folder,
341
+ the library's tests leave nothing in it.
342
+
3
343
  ## 0.3.0 (10 October 2026)
4
344
 
5
345
  - 2026-10-10: **a development stand-in for a passkey**, `provared/dev`
package/README.md CHANGED
@@ -1,4 +1,4 @@
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
 
@@ -72,7 +72,19 @@ The same in Python is
72
72
  `from provared.dev import development_recorder`. For a LangChain agent,
73
73
  `pip install provared-langchain` puts every tool call behind the same
74
74
  check with no change to the tools
75
- ([python/integrations/provared-langchain](python/integrations/provared-langchain)).
75
+ ([python/integrations/provared-langchain](python/integrations/provared-langchain)):
76
+
77
+ ```py
78
+ from langchain.agents import create_agent
79
+ from provared_langchain import ProvaredMiddleware
80
+
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
87
+ ```
76
88
 
77
89
  ## The four parts
78
90
 
@@ -129,7 +141,12 @@ each part protects against, is in
129
141
  in plain words for a reader who does not write code.
130
142
  - **[The checking page](https://prova.red/check/)**, which checks a
131
143
  record in a browser and loads nothing from anywhere; also built as
132
- one file, attached to each release.
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.
133
150
  - **The core of the format as an individual Internet-Draft** at the
134
151
  IETF:
135
152
  [draft-izmaylov-agent-permission-receipts](https://datatracker.ietf.org/doc/draft-izmaylov-agent-permission-receipts/).
@@ -139,12 +156,13 @@ each part protects against, is in
139
156
 
140
157
  ## Status
141
158
 
142
- **Version 0.3.0, a draft.** Published as it is, by one author in their
159
+ **Version 0.5.0, a draft.** Published as it is, by one author in their
143
160
  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.
161
+ allows. The code has been through 50 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.
148
166
  Every published version stays on npm and the Python Package Index, so a
149
167
  record made with one version can always be checked by that version.
150
168
 
@@ -457,7 +457,9 @@ function named(s) {
457
457
  }
458
458
 
459
459
  function trustWords(s) {
460
- if (named(s)) return s.counts.seals ? 'yes: the passkey you trust, and the recorder you expect' : 'yes: the passkey you trust';
460
+ const passkeys = new Set(args.issuerKeys).size === 1 ? 'the passkey you trust' : 'the passkeys you trust';
461
+ const recorders = new Set(args.sealKeys).size === 1 ? 'the recorder you expect' : 'the recorders you expect';
462
+ if (named(s)) return s.counts.seals ? `yes: ${passkeys}, and ${recorders}` : `yes: ${passkeys}`;
461
463
  if (args.issuerKeys.length === 0) return 'NO: you named no passkey you trust (--issuer), so anyone could have made this record. This is not a pass.';
462
464
  return 'NO: the record has a seal, and you named no recorder you expect (--sealer), so anyone could have sealed it. This is not a pass.';
463
465
  }