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 +101 -0
- package/README.md +2 -2
- package/docs/guide.md +179 -3
- package/docs/threat-model.md +73 -0
- package/package.json +1 -1
- package/spec/provared-format.md +123 -1
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +149 -0
- package/src/guard.js +1 -1
- package/src/index.js +8 -3
- package/src/pass.js +1 -1
- package/src/recorder.js +137 -11
- package/src/seal.js +4 -3
- package/src/service.js +4 -4
- package/src/signatures.js +276 -1
- package/src/standing.js +4 -4
- package/src/stub.js +18 -5
- package/src/types.d.ts +58 -12
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.
|
|
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
|
|
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.
|
|
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.
|
|
154
|
+
version 0.5.0 is attached to the release on GitHub. Its SHA-256
|
|
155
155
|
fingerprint is
|
|
156
|
-
`
|
|
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
|
|
package/docs/threat-model.md
CHANGED
|
@@ -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.
|
|
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",
|
package/spec/provared-format.md
CHANGED
|
@@ -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]
|