provared 0.4.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,106 @@
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
+
3
104
  ## 0.4.0 (14 October 2026)
4
105
 
5
106
  - **The command-line checker counts the keys it was given.** With two
package/README.md CHANGED
@@ -156,9 +156,9 @@ each part protects against, is in
156
156
 
157
157
  ## Status
158
158
 
159
- **Version 0.4.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
160
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,
161
+ allows. The code has been through 50 rounds of review by AI reviewers,
162
162
  each reviewer a fresh session given the code, the documents and a brief
163
163
  of what to look at; every fault was fixed, or stated as a limit, and
164
164
  kept as a test. No person other than the author has reviewed it yet.
package/docs/guide.md CHANGED
@@ -6,7 +6,7 @@ Everything about the format, the library, the checker, the stub writer beside an
6
6
 
7
7
  ## Status
8
8
 
9
- **Version 0.4.0, a draft.** Published as it is, by one author in their
9
+ **Version 0.5.0, a draft.** Published as it is, by one author in their
10
10
  own time; fixes as time allows. The format may still change, so do not
11
11
  rely on a draft record staying checkable.
12
12
 
@@ -151,9 +151,9 @@ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
151
151
  so that a copy can be compared with it. (Tried on 8 October 2026 in the
152
152
  three browser builds this repository is tested with, and on 13 October
153
153
  2026 in Chrome 155, Edge 155 and Brave 155.) The file for
154
- version 0.4.0 is attached to the release on GitHub. Its SHA-256
154
+ version 0.5.0 is attached to the release on GitHub. Its SHA-256
155
155
  fingerprint is
156
- `49da6617ff493ddda2b2b65bfb6bedebdf8cf07ea6faa32b039f065eda207307`.
156
+ `cec1b8d5e6e7a11c00478c6e83d9f9b5963340ec656657ea0ac84f32e8e52bda`.
157
157
 
158
158
  **Run the demonstration in a browser.**
159
159
 
@@ -551,6 +551,77 @@ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
551
551
  though the tool has run and its stub is in the book. `recordTools`
552
552
  names no agent software, and works with any function.
553
553
 
554
+ ### Binding a stub to the platform's own record
555
+
556
+ Where the other side will not countersign, a stub can still be matched
557
+ with the other system's own log of the call: name, among its documents,
558
+ the fingerprint of an identifier that both records hold. Document names
559
+ that begin `provared.` are reserved for this (format description,
560
+ section 27); any other such name is refused.
561
+
562
+ | Name | The fingerprint of | Made with |
563
+ |---|---|---|
564
+ | `provared.arguments` | The call's arguments, in the one form of RFC 8785 | `argumentsFingerprint` |
565
+ | `provared.http-request` | The request: its method, its address exactly as sent, and its body where it has one; no header | `httpRequestFingerprint` |
566
+ | `provared.http-response` | The answer: its status code, and its body where it has one; no header | `httpResponseFingerprint` |
567
+ | `provared.trace-id` | The trace-id of the W3C Trace Context `traceparent` header the agent's side sent (its form is not checked) | `identifierFingerprint` |
568
+ | `provared.request-id` | An identifier the agent's side chose and sent with the call | `identifierFingerprint` |
569
+ | `provared.event-id` | An identifier the platform gave for the call | `identifierFingerprint` |
570
+ | `provared.error` | The error text the agent's software was given | `identifierFingerprint` |
571
+
572
+ `DOCUMENT_NAMES` lists them, each with the exact text it stands for. An
573
+ HTTP request or answer is written as the signature base of RFC 9421
574
+ (nothing is signed with it): the address is taken exactly as it was
575
+ sent, never rewritten, so give it in the form that was sent. A body is
576
+ the bytes RFC 9530 digests: after any content coding (such as gzip),
577
+ before any transfer coding; a body of `null` is no body. The three
578
+ identifiers may be named more than once in a stub; every other reserved
579
+ name once at most. A trace-id is fingerprinted as given: its form (32
580
+ lower-case hexadecimal characters in the W3C standard) is not checked.
581
+
582
+ An identifier that the platform gives only in its answer cannot be in a
583
+ stub signed before the action. `act` takes `detailsAfter` for it:
584
+
585
+ ```js
586
+ import { identifierFingerprint } from 'provared';
587
+
588
+ const outcome = await recorder.act(
589
+ { action: 'provared.data.change', with: 'tickets' },
590
+ async () => updateTheTicket(), // gives the platform's answer
591
+ { detailsAfter: async (answer) => [{ name: 'provared.event-id', sha256: await identifierFingerprint(answer.requestId) }] },
592
+ );
593
+ outcome.detailsAfter; // { added: 1, problem: null }
594
+ ```
595
+
596
+ The stub is still written and checked with the whole book before the
597
+ action, so it is known to fit. After the action it is signed again with
598
+ up to four further documents, within the same call, so that no other
599
+ call comes between; it keeps the time the action began. If
600
+ `detailsAfter` fails, gives no answer in the time the other side is
601
+ given to countersign (`countersignWithin`), gives more than four
602
+ documents, or gives one that would be refused, the stub is written
603
+ without them, and `outcome.detailsAfter.problem` says why: an action
604
+ that was taken always gets its stub. The request may then name at most
605
+ 28 documents of its own, and no approval may be given, because an
606
+ approval covers exactly the documents the person saw. `detailsAfter`
607
+ must not call the writer.
608
+
609
+ A matching identifier shows that the two records speak of one call, not
610
+ that the call did what either says: the platform's identifier is the
611
+ platform's word. It binds the two only where the platform writes the
612
+ same identifier into its own log; which one a platform keeps is for you
613
+ to find out, so record both one you sent and one you were given where
614
+ you can. Only an identifier picks out one call: a fingerprint of a
615
+ request, an answer, the arguments or an error shows only that the
616
+ content matches, since identical requests, and identical answers (many a
617
+ `200` with the body `{}`), share one fingerprint. A platform that logs
618
+ the decoded body of a compressed message will not match the body's
619
+ fingerprint. An identifier that is short or easy to guess can be guessed
620
+ from its fingerprint.
621
+
622
+ `recordTools` still names the arguments `arguments` by default; a tool
623
+ can name them `provared.arguments` through its own `details` function.
624
+
554
625
  ## How signing works
555
626
 
556
627
  - **The person** signs a slip with a passkey. A passkey signs a short
@@ -575,6 +646,107 @@ names no agent software, and works with any function.
575
646
  signatures were not checked, and does not show a pass. Node.js checks
576
647
  all of them.
577
648
 
649
+ ### Keys held elsewhere: signers
650
+
651
+ The keys of an agent, a service, an organisation or a recorder need not
652
+ sit in the memory of the program that writes the records. Wherever a
653
+ writer takes `privateKeys` (`openRecorder`, `writeStub`, `countersign`,
654
+ `writeRefusal`, `writeTerms`, `writePass`, `writeAcknowledgement`,
655
+ `writeVouching`, `writeWithdrawal` and `writeSeal`), each item may be a
656
+ key, as before, or a **signer**: an object with two members.
657
+
658
+ - `key`: the public key, as a JSON Web Key in one of the shapes the
659
+ format allows (format description, section 3.8).
660
+ - `sign(data)`: a promise of the signature over exactly those bytes, as a
661
+ `Uint8Array` or an `ArrayBuffer`.
662
+
663
+ A signer lets a key live in a key service or a hardware module. Keys and
664
+ signers may be mixed: an agent may keep its ML-DSA-87 key in a key
665
+ service and its Ed25519 key elsewhere. `signerFromKey(alg, privateKey,
666
+ key)` wraps a key held in memory as a signer, so that both take one path.
667
+
668
+ ```js
669
+ import { openRecorder, signerFromKey } from 'provared';
670
+
671
+ // "keyService.sign" stands for the service's own call. It must sign the
672
+ // bytes themselves, not a fingerprint of them.
673
+ const held = {
674
+ key: agentKeys[1], // the agent's ML-DSA-87 public key, as the slip names it
675
+ sign: (data) => keyService.sign('agent-ml-dsa-87', data),
676
+ };
677
+ const recorder = await openRecorder({
678
+ book, slip, issuerKeys,
679
+ privateKeys: [signerFromKey('Ed25519', ed25519PrivateKey, agentKeys[0]), held],
680
+ signWithin: 10000, // milliseconds for each signature; 30,000 by default
681
+ });
682
+ ```
683
+
684
+ A record made through a signer is the same record as one made with the
685
+ key itself: the same content, the same labels and the same Ed25519
686
+ signature. ML-DSA-87 and SLH-DSA signatures are made afresh each time,
687
+ with a key or through a signer, so those differ between any two
688
+ signings, as they always did.
689
+
690
+ A signer is not taken on trust:
691
+
692
+ - Its two members are read once. Its key must be exactly a key of the
693
+ method of its place in the list, as plain data.
694
+ - A signer for a method this device does not have built in is refused
695
+ before any signer is asked to sign, since its signatures could not be
696
+ checked here.
697
+ - Where the writer knows the key the record must be signed with, the
698
+ signer's key must be that key: the agent's keys as the slip names them,
699
+ or a helper's as its pass names them, in the stub writer; the keys
700
+ `by` names in `writeSeal`, `writeRefusal`, `writeTerms` and
701
+ `writeVouching`. A signer for another key is refused (`bad-key`)
702
+ before any signer is asked to sign. This catches the most likely
703
+ fault of a key held elsewhere: a mistyped key name in its settings.
704
+ The stub writer compares when it is opened, with the slip, or the
705
+ pass it acts under, as its book holds it. Opened with a pass that its
706
+ book does not yet hold, it compares nothing then, and refuses each
707
+ action (`pass-missing`), as it always did.
708
+ - `writeStub`, `countersign`, `writePass`, `writeAcknowledgement` and
709
+ `writeWithdrawal`, used by themselves, cannot know which key the
710
+ record must be signed with. There a signer for another key gives a
711
+ record that fails its own check, as a wrong key in memory always did.
712
+ - Every signature it gives is checked against its key before it is
713
+ used. A signature of the wrong length, one that does not check
714
+ (another key used, or other bytes signed, such as a fingerprint of
715
+ them), something other than bytes, or a `sign` that throws: no record
716
+ is made (`signature-invalid`), nothing is written to a book, and the
717
+ error names the place in the list, the method and the key's
718
+ thumbprint.
719
+ - In the stub writer, a signer is given `signWithin` milliseconds for
720
+ each signature (30,000 by default). After that the action is not
721
+ taken, nothing is written, the writer goes on to its next call, and a
722
+ late answer is not used. A signer that calls the writer is refused at
723
+ once, as an action that does so is. The other writers set no time of
724
+ their own: a signer that never answers leaves the call waiting, so give
725
+ the signer's own connection a time limit.
726
+
727
+ Where no record is made, signers earlier in the list may already have
728
+ signed, and a signer that ran out of time may still sign later. So a key
729
+ service's own log may hold signatures that match no record.
730
+
731
+ A key given as before is used exactly as before: a private Web Crypto
732
+ `CryptoKey` of the method of its place and, in a seal's SLH-DSA place, a
733
+ private Node.js `KeyObject`, or what Node.js reads as one (PEM text, DER
734
+ bytes with their format, or a JSON Web Key with its format). Anything
735
+ else that is not a signer (a public key, a key of another method or
736
+ kind, a value that only looks like a key) is refused (`bad-field`).
737
+
738
+ What a signer does not change:
739
+
740
+ - **The library cannot know how a key held elsewhere is kept**, who else
741
+ can ask for signatures with it, or whether the service keeps it safe.
742
+ It checks only that each signature fits the key the signer names.
743
+ - **A record does not say where a key was kept.** A record signed
744
+ through a key service reads the same as one signed with a key in
745
+ memory, and a key set split between two places reads as one agent.
746
+ - **Each signature costs one more check.** Measured on one computer on
747
+ 15 October 2026: about 0.3 thousandths of a second for Ed25519, 0.6
748
+ for ML-DSA-87 and 2 for SLH-DSA-SHA2-256s.
749
+
578
750
  ## Using the library
579
751
 
580
752
  ```js
@@ -706,6 +878,10 @@ prepared, then `assembleSlip`; keep the disclosures it prepared.
706
878
  - A name in a record is a label. Only keys are checked. A vouching
707
879
  record shows which organisation stood behind a name, not how well it
708
880
  checked.
881
+ - An identifier of another system's, named in a stub, shows that the
882
+ two records speak of one call, not that the call did what either
883
+ says. A fingerprint of a request, an answer, the arguments or an error
884
+ shows only that the content matches, not that it was the same call.
709
885
 
710
886
  And about time:
711
887
 
@@ -123,6 +123,60 @@ fingerprint.
123
123
  - The documents themselves. A stub holds fingerprints of documents, not
124
124
  the documents. Whoever keeps the documents must keep them.
125
125
 
126
+ **A stub bound to the other system's own record of the call.** Where the
127
+ other side will not countersign, a stub may name, under a reserved name
128
+ (format description, section 27), the fingerprint of the identifier the
129
+ platform gave the call (`provared.event-id`), of one the agent's side
130
+ chose and sent (`provared.trace-id`, `provared.request-id`), of part
131
+ of the request and the answer (`provared.http-request`: the method, the
132
+ address and the body; `provared.http-response`: the status and the
133
+ body; no header is covered), or of the error the agent was given
134
+ (`provared.error`).
135
+
136
+ - *What it shows.* Someone who holds the platform's own log can work the
137
+ fingerprint out again and match the two records, without either
138
+ side's help. For an identifier, the two speak of one call. An agent's
139
+ side that later claims another identifier for the call, or a platform
140
+ that later shows another, is caught by the mismatch.
141
+ - *Only an identifier picks out one call.* A request, an answer, the
142
+ arguments or an error shows only that the content matches: identical
143
+ requests, and identical answers (many a `200` with the body `{}`),
144
+ share one fingerprint, so a match with one of them does not show that
145
+ it was the same call. Only `provared.trace-id`, `provared.request-id`
146
+ and `provared.event-id` are meant to pick out one call, and only they
147
+ may be named more than once in a stub.
148
+ - *The body's bytes* are what RFC 9530 digests: the content after any
149
+ content coding (such as gzip) and before any transfer coding. A
150
+ platform that logs the decoded body of a message sent compressed will
151
+ not match.
152
+ - *A trace-id's form is not checked.* The W3C standard writes it as 32
153
+ lower-case hexadecimal characters, but any text is fingerprinted as
154
+ given.
155
+ - *What it does not show.* That the call did what either record says:
156
+ the platform's identifier is the platform's word, and the stub is
157
+ still the agent's side's word. That the platform keeps the identifier
158
+ at all: which one a platform writes into its log differs from one
159
+ platform to another, and is for whoever relies on it to find out. That
160
+ the fingerprint was made from the text the name says: a checker cannot
161
+ tell; only someone who holds that text can. An identifier that is
162
+ short or easy to guess gives no privacy through its fingerprint.
163
+ - *Against A1 and A3* (the agent's own side). The binding is written by
164
+ the agent's side, so a dishonest one can leave it out, or name a
165
+ wrong identifier; only the match with the platform's log, made later,
166
+ catches the second. It adds nothing against an agent that writes no
167
+ stub.
168
+ - *Two spellings of one address.* An HTTP address is taken exactly as it
169
+ was sent and never rewritten, so that two programs fingerprint the
170
+ same request the same way. A program that records a rewritten address
171
+ (`%2f` for `%2F`, a host in capitals) makes a fingerprint that will not
172
+ match: whoever writes the stub must give the address as sent.
173
+ - *A reserved name not on the list* is refused (`bad-field`), so that a
174
+ name that may be given a meaning later cannot be used now with another;
175
+ so is one of the four once-only names named twice (two `provared.arguments`
176
+ fingerprints in one stub would leave unclear which was the call's). A
177
+ book written before this rule that holds such a name no longer passes
178
+ its check.
179
+
126
180
  ## 4a. Limits, conditions, approvals and the other side's records
127
181
 
128
182
  **Protects.** That what the person allowed is stated exactly, and that a
@@ -235,6 +289,25 @@ disagree about. Totals are added as whole numbers of any size.
235
289
  of the arguments fixes what was asked for without holding it; where
236
290
  the arguments are few and easy to guess, the fingerprint can be
237
291
  guessed.
292
+ - **Documents named after the action** (`detailsAfter` in the stub
293
+ writer's `act`). The stub is first written and checked with the whole
294
+ book before the action, as always; after the action, it is signed
295
+ again with up to four further documents, still within the same call,
296
+ so that no other call comes between, and keeps the time the action
297
+ began. The room for them is kept beforehand: with today's limits the
298
+ largest stub, with 32 documents of the longest names, and a
299
+ countersignature still fits a line, and the writer checks this again
300
+ for each such call. If what `detailsAfter` gives cannot go in (it
301
+ fails, gives no answer within the time the other side is given to
302
+ countersign, gives more than four, or gives a document that would be
303
+ refused), the stub made before the action is written without them,
304
+ and the answer says why: an action that was taken always gets its
305
+ stub. What it gives is copied when it is read, so a later change to
306
+ it changes nothing. A call to the writer from inside it is refused at
307
+ once. It cannot be used with an approval, because an approval covers
308
+ exactly the documents the person saw. The stub is signed twice for
309
+ such an action, and only one of the two is written: a signer kept
310
+ elsewhere is asked once more.
238
311
  - **The check before acting refuses more in one case.** An action that
239
312
  would add to a period which a stub dated after it already overfills is
240
313
  refused, though a checker would report nothing new for it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "provared",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Proof of what an AI agent was allowed to do, and what it then did. A passkey-signed permission with limits a machine can check, a signed receipt for each action, and a free checker. Evidence, not a verdict.",
5
5
  "author": "Pavel Izmaylov",
6
6
  "license": "Apache-2.0 AND CC-BY-4.0",
@@ -440,7 +440,10 @@ checker keeps no state.
440
440
  | `pass` | For a helper agent: the pass it acts under. Optional | The fingerprint of the pass (section 25) |
441
441
  | `when` | When it was done | A time |
442
442
 
443
- A stub holds fingerprints of documents, never the documents.
443
+ A stub holds fingerprints of documents, never the documents. A document
444
+ `name` is a label chosen by whoever wrote the stub, except that names
445
+ that begin `provared.` are reserved (section 27). The same label MAY
446
+ appear more than once; section 27 says which reserved names may.
444
447
 
445
448
  ### 5.2 Signing and checking
446
449
 
@@ -658,6 +661,11 @@ the seal's own date (section 21.1, rule 4).
658
661
  that the organisation checked well.
659
662
  - The fingerprints of an agent's software in a slip are a statement of
660
663
  what the person approved, not proof of what ran.
664
+ - A reserved identifier that binds a stub to another system's own
665
+ record of the call (section 27) shows that the two records speak of
666
+ one call, not that the call did what either says. A matching
667
+ fingerprint of a request, an answer, arguments or an error shows only
668
+ that the content matches, not that it was the same call.
661
669
 
662
670
  ## 14. Standards used
663
671
 
@@ -679,6 +687,8 @@ the seal's own date (section 21.1, rule 4).
679
687
  | RFC 3161; RFC 5652; RFC 5280; ITU-T X.690 | Time-stamps: the statement, its signed wrapper, the certificate, and the byte layout (DER) |
680
688
  | The OpenTimestamps proof file, version 1 (an open format; not the work of a standards body) | Block time-stamps: the steps from a fingerprint to the Merkle root of a block |
681
689
  | RFC 9901 (Selective Disclosure for JSON Web Tokens) | Covered fields |
690
+ | RFC 9421 (HTTP Message Signatures), section 2.5, only its way of writing a message as text; RFC 9530 (Digest Fields); RFC 9110, section 5.6.2 | The texts that the reserved document names `provared.http-request` and `provared.http-response` stand for (section 27). Nothing is signed with RFC 9421 |
691
+ | W3C Trace Context (a W3C Recommendation) | The trace-id that `provared.trace-id` stands for (section 27) |
682
692
 
683
693
  ## 15. Choices that go beyond the standards
684
694
 
@@ -735,6 +745,12 @@ is frozen.
735
745
  pass.
736
746
  18. The acknowledgement of section 26 and its label
737
747
  `vnd.provared.acknowledgement.v0+json`.
748
+ 19. The reserved document names of section 27, and the exact texts their
749
+ fingerprints are made from: an HTTP address taken exactly as it was
750
+ sent, never rewritten; a body that is present but empty given a
751
+ Content-Digest, and an absent body none; no header covered; SHA-256
752
+ only; only the three identifiers may be named more than once; the
753
+ form of a trace-id not checked.
738
754
 
739
755
  ## 16. The shared list: actions, kinds of action, rules of conduct
740
756
 
@@ -1718,6 +1734,112 @@ section 22. Nothing obliges an agent's side to acknowledge: a
1718
1734
  cancellation with no acknowledgement shows nothing about whether the
1719
1735
  agent's side was told.
1720
1736
 
1737
+ ## 27. Reserved document names
1738
+
1739
+ Where the other side of an action will not countersign, the next best
1740
+ binding is the other system's own identifier for the call, named in the
1741
+ stub by its fingerprint. An investigator who holds that system's own log
1742
+ can then work out the fingerprint again and match the two records,
1743
+ without either side's help. Only an identifier is meant to pick out one
1744
+ call. A stub may also name fingerprints of some of what was sent and
1745
+ received (the arguments; an HTTP request's method, address and body; an
1746
+ answer's status and body); those show that the content matches, not
1747
+ that it was the same call.
1748
+
1749
+ So document names (section 5.1) that begin `provared.` are reserved. A
1750
+ document name that begins `provared.` and is not in this list MUST be
1751
+ refused (`bad-field`), in a stub, an approval (section 17) and the
1752
+ check before acting (section 20). The comparison is exact: a name in
1753
+ other letters, such as `Provared.invoice`, is a label like any other.
1754
+ `provared.trace-id`, `provared.request-id` and `provared.event-id` MAY
1755
+ each appear more than once in one stub (identifiers from two systems,
1756
+ say); every other name in the list MUST appear at most once, and a
1757
+ record that names one of them twice MUST be refused (`bad-field`), in
1758
+ the same places. The `sha256` of each is the SHA-256, in base64url, of
1759
+ exactly this text, in UTF-8:
1760
+
1761
+ | Name | The text whose SHA-256 is given |
1762
+ |---|---|
1763
+ | `provared.arguments` | The call's arguments, written in the one form of RFC 8785 |
1764
+ | `provared.http-request` | The request, written as below, over `"@method"`, `"@target-uri"` and, where there is a body, `"content-digest"` |
1765
+ | `provared.http-response` | The answer, written as below, over `"@status"` and, where there is a body, `"content-digest"` |
1766
+ | `provared.trace-id` | The trace-id of the W3C Trace Context `traceparent` header that the agent's side sent, exactly as sent. That standard writes it as 32 lower-case hexadecimal characters; this format does not check the form, and a checker cannot |
1767
+ | `provared.request-id` | An identifier that the agent's side chose and sent with the call, exactly as sent |
1768
+ | `provared.event-id` | An identifier that the platform gave for the call, exactly as it gave it |
1769
+ | `provared.error` | The error text that the agent's software was given, exactly as given |
1770
+
1771
+ An identifier or an error text MUST be one character or more, and
1772
+ well-formed Unicode.
1773
+
1774
+ **How a request or an answer is written.** As the signature base of RFC
1775
+ 9421 (HTTP Message Signatures), section 2.5, with no signature
1776
+ parameters. RFC 9421 is used only for this exact way of writing a
1777
+ message as text; nothing is signed with it. For each component, in the
1778
+ order given: a line `"<component>": <value>` and a line feed (0x0A);
1779
+ then the line `"@signature-params": (` with the components, each in
1780
+ double quotation marks and separated by one space, and `)`, with no line
1781
+ feed after it. The values:
1782
+
1783
+ - `"@method"`: the method exactly as sent, a token of RFC 9110, section
1784
+ 5.6.2. Its letters are not changed, as RFC 9421, section 2.2.1, sets
1785
+ out.
1786
+ - `"@target-uri"`: the absolute address exactly as sent. It MUST begin
1787
+ `http://` or `https://`, hold only the visible characters of ASCII
1788
+ (0x21 to 0x7E), and hold no fragment (`#`). It is never rewritten:
1789
+ two spellings of one address (`%2F` and `%2f`, a host in capitals, a
1790
+ port written or left out) give two fingerprints. Whoever writes the
1791
+ stub MUST give the address in the form that was sent.
1792
+ - `"@status"`: the status code of the answer, a whole number from 100 to
1793
+ 599, written in digits.
1794
+ - `"content-digest"`: present only where the message has a body; a body
1795
+ that is present and empty has one. The value is the field value of RFC
1796
+ 9530 with SHA-256 only: `sha-256=:`, the SHA-256 of the body's bytes,
1797
+ in base64 with padding (RFC 4648, section 4), and `:`. The body's
1798
+ bytes are what RFC 9530 digests: the content after any content coding
1799
+ (such as gzip) and before any transfer coding. A system that keeps
1800
+ the decoded body, where the body was sent compressed, will not match.
1801
+
1802
+ No header is covered, and nothing else of the message: two messages
1803
+ that differ only in their headers give one fingerprint.
1804
+
1805
+ For example, the request `POST https://example.com/foo?param=Value&Pet=dog`
1806
+ with the body `{"hello": "world"}` is written as these four lines:
1807
+
1808
+ ```
1809
+ "@method": POST
1810
+ "@target-uri": https://example.com/foo?param=Value&Pet=dog
1811
+ "content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
1812
+ "@signature-params": ("@method" "@target-uri" "content-digest")
1813
+ ```
1814
+
1815
+ **What a binding shows, and what it does not.**
1816
+
1817
+ - An identifier binds the two records only where the other system writes
1818
+ the same identifier into its own log. Which one a system keeps differs
1819
+ from one system to another, and is for whoever relies on it to find
1820
+ out. Recording both kinds, one the agent's side chose and sent
1821
+ (`provared.trace-id`, `provared.request-id`) and one the platform gave
1822
+ back (`provared.event-id`), gives the better chance of a match.
1823
+ - The platform's identifier is the platform's word. A matching
1824
+ identifier shows that the two records speak of one call, not that the
1825
+ call did what either says.
1826
+ - Only `provared.trace-id`, `provared.request-id` and `provared.event-id`
1827
+ are meant to pick out one call. `provared.arguments`,
1828
+ `provared.http-request`, `provared.http-response` and `provared.error`
1829
+ show only that the content matches: identical requests, and identical
1830
+ answers (many a `200` with the body `{}`), share one fingerprint, so a
1831
+ match with one of them does not show that it was the same call.
1832
+ - A fingerprint of an identifier, or of arguments, that is short or easy
1833
+ to guess can be guessed: it hides nothing from someone who can try
1834
+ the likely values.
1835
+ - A checker cannot see whether a fingerprint was made from the text this
1836
+ section names: only someone who holds that text can work it out again.
1837
+ - Records written before this section was added may use names that
1838
+ begin `provared.` that are not in the list, or name one of the four
1839
+ once-only names twice; such records are refused, so a book that holds
1840
+ one no longer passes its check. A checker that does not know this
1841
+ section reads a reserved name as a label.
1842
+
1721
1843
  ---
1722
1844
 
1723
1845
  Copyright 2026 Pavel Izmaylov. This document is licensed under the
package/src/dev.d.ts CHANGED
@@ -50,5 +50,5 @@ export function developmentSlip(fields: DevelopmentSlipFields): Promise<Developm
50
50
  /** The stub writer opened under a development slip, in one call. */
51
51
  export function developmentRecorder(
52
52
  fields: DevelopmentSlipFields,
53
- more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number },
53
+ more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number; signWithin?: number },
54
54
  ): Promise<Omit<DevelopmentSlip, 'book'> & { recorder: Recorder }>;
package/src/dev.js CHANGED
@@ -110,7 +110,7 @@ export async function developmentSlip(fields = {}) {
110
110
  * The stub writer, opened under a development slip, in one call: the
111
111
  * shortest way to a first record. The same as developmentSlip followed by
112
112
  * openRecorder; "more" is passed to openRecorder (options, now,
113
- * countersignWithin).
113
+ * countersignWithin, signWithin).
114
114
  *
115
115
  * @param {object} fields the members of the slip; "actions" is required
116
116
  * @param {object} [more]